ASP.NET Core Web API异常处理

And*_*rei 230 c# exception asp.net-core

在使用常规ASP.NET Web API多年后,我开始使用ASP.NET Core作为我的新REST API项目.我没有看到在ASP.NET Core Web API中处理异常的好方法.我试图实现异常处理过滤器/属性:

public class ErrorHandlingFilter : ExceptionFilterAttribute
{
    public override void OnException(ExceptionContext context)
    {
        HandleExceptionAsync(context);
        context.ExceptionHandled = true;
    }

    private static void HandleExceptionAsync(ExceptionContext context)
    {
        var exception = context.Exception;

        if (exception is MyNotFoundException)
            SetExceptionResult(context, exception, HttpStatusCode.NotFound);
        else if (exception is MyUnauthorizedException)
            SetExceptionResult(context, exception, HttpStatusCode.Unauthorized);
        else if (exception is MyException)
            SetExceptionResult(context, exception, HttpStatusCode.BadRequest);
        else
            SetExceptionResult(context, exception, HttpStatusCode.InternalServerError);
    }

    private static void SetExceptionResult(
        ExceptionContext context, 
        Exception exception, 
        HttpStatusCode code)
    {
        context.Result = new JsonResult(new ApiResponse(exception))
        {
            StatusCode = (int)code
        };
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我的启动过滤器注册:

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});
Run Code Online (Sandbox Code Playgroud)

我遇到的问题是,当我AuthorizationFilter的异常发生时,它没有被处理ErrorHandlingFilter.我希望它能够像旧的ASP.NET Web API一样被捕获.

那么如何捕获所有应用程序异常以及Action Filters的任何异常?

And*_*rei 463

异常处理中间件

经过多次使用不同异常处理方法的实验后,我最终使用了中间件.它最适合我的ASP.NET Core Web API应用程序.它处理应用程序异常以及来自过滤器的异常,我可以完全控制异常处理并创建响应json.这是我的异常处理中间件:

public class ErrorHandlingMiddleware
{
    private readonly RequestDelegate next;
    public ErrorHandlingMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private static Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        var code = HttpStatusCode.InternalServerError; // 500 if unexpected

        if      (ex is MyNotFoundException)     code = HttpStatusCode.NotFound;
        else if (ex is MyUnauthorizedException) code = HttpStatusCode.Unauthorized;
        else if (ex is MyException)             code = HttpStatusCode.BadRequest;

        var result = JsonConvert.SerializeObject(new { error = ex.Message });
        context.Response.ContentType = "application/json";
        context.Response.StatusCode = (int)code;
        return context.Response.WriteAsync(result);
    }
}
Run Code Online (Sandbox Code Playgroud)

在课堂上的MVC之前注册Startup:

app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseMvc();
Run Code Online (Sandbox Code Playgroud)

以下是异常响应的示例:

{ "error": "Authentication token is not valid." }
Run Code Online (Sandbox Code Playgroud)

您可以添加堆栈跟踪,异常类型名称,错误代码或任何您想要的内容.非常灵活.希望这对你来说是一个很好的起点!

  • 我一直在试图让自定义中间件工作到今天的桌子上,它的工作方式基本相同(我用它来管理工作单元/事务处理请求).我面临的问题是,"next"中引发的异常并未在中间件中被捕获.你可以想象,这是有问题的.我做错了什么/错过了什么?任何指针或建议? (4认同)
  • @ brappleye3 - 我弄清楚问题是什么.我只是在Startup.cs类的错误位置注册中间件.我把`app.UseMiddleware <ErrorHandlingMiddleware>();`移到`app.UseStaticFiles();`之前.现在似乎正确地捕获了异常.这让我相信`app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); app.UseBrowserLink();`做一些内部魔术中间件hackery以使中间件订购正确. (4认同)
  • 我同意自定义中间件可能非常有用,但会对NotFound,Unauthorized和BadRequest情况使用异常提出质疑.为什么不简单地设置状态代码(使用NotFound()等)然后在自定义中间件或通过UseStatusCodePagesWithReExecute处理它?有关详细信息,请参阅https://www.devtrends.co.uk/blog/handling-errors-in-asp.net-core-web-api (4认同)
  • 我通常混合使用中间件和`IExceptionFilter`.过滤器直接处理控制器错误,我使用中间件进行更"低级"处理.作为提示,如果有人需要在全局处理程序中按异常类型执行代码,为了使其更具"可读性",请随时查看我为此制作的小型库:https://medium.com/@ nogravity00/ASP净芯MVC-和异常处理,f0da1c820d4a (3认同)
  • 这很糟糕,因为它总是序列化为JSON,完全忽略了内容协商. (3认同)
  • @Konrad有效点。这就是为什么我说这个示例是您开始的地方,而不是最终结果。对于99%的API,JSON绰绰有余。如果您觉得这个答案不够好,请随时做出贡献。 (2认同)

Ily*_*dik 30

Latest Asp.Net Core (at least from 2.2, probably earlier) has a built-in middleware that makes it a bit easier compared to the implementation in the accepted answer:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;

    var result = JsonConvert.SerializeObject(new { error = exception.Message });
    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}));
Run Code Online (Sandbox Code Playgroud)

It should do pretty much the same, just a bit less code to write. Remember to add it before UseMvc as order is important.

  • 它是否支持 DI 作为处理程序的参数,或者必须在处理程序中使用服务定位器模式? (2认同)

Ash*_*Lee 27

您最好的选择是使用中间件来实现您正在寻找的日志记录.您希望将异常日志记录放在一个中间件中,然后处理在不同中间件中向用户显示的错误页面.这允许逻辑分离并遵循微软已经用2个中间件组件设计的设计.这是微软文档的一个很好的链接:ASP.Net Core中的错误处理

对于您的具体示例,您可能希望使用StatusCodePage中间件中的一个扩展,或者像这样使用自己的扩展.

您可以在此处找到用于记录异常的示例:ExceptionHandlerMiddleware.cs

public void Configure(IApplicationBuilder app)
{
    // app.UseErrorPage(ErrorPageOptions.ShowAll);
    // app.UseStatusCodePages();
    // app.UseStatusCodePages(context => context.HttpContext.Response.SendAsync("Handler, status code: " + context.HttpContext.Response.StatusCode, "text/plain"));
    // app.UseStatusCodePages("text/plain", "Response, status code: {0}");
    // app.UseStatusCodePagesWithRedirects("~/errors/{0}");
    // app.UseStatusCodePagesWithRedirects("/base/errors/{0}");
    // app.UseStatusCodePages(builder => builder.UseWelcomePage());
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");  // I use this version

    // Exception handling logging below
    app.UseExceptionHandler();
}
Run Code Online (Sandbox Code Playgroud)

如果你不喜欢那个特定的实现,那么你也可以使用ELM Middleware,这里有一些例子:Elm异常中间件

public void Configure(IApplicationBuilder app)
{
    app.UseStatusCodePagesWithReExecute("/Errors/{0}");
    // Exception handling logging below
    app.UseElmCapture();
    app.UseElmPage();
}
Run Code Online (Sandbox Code Playgroud)

如果这不能满足您的需求,您可以通过查看ExceptionHandlerMiddleware和ElmMiddleware的实现来总是推出自己的中间件组件,以掌握构建自己的概念.

在StatusCodePages中间件下面添加异常处理中间件,但最重要的是在其他所有中间件组件上添加.这样你的Exception中间件将捕获异常,记录它,然后允许请求进入StatusCodePage中间件,它将向用户显示友好的错误页面.

  • 现在链接已断开。 (2认同)

Iha*_*ush 18

要为每个异常类型配置异常处理行为,您可以使用NuGet包中的中间件:

代码示例:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddExceptionHandlingPolicies(options =>
    {
        options.For<InitializationException>().Rethrow();

        options.For<SomeTransientException>().Retry(ro => ro.MaxRetryCount = 2).NextPolicy();

        options.For<SomeBadRequestException>()
        .Response(e => 400)
            .Headers((h, e) => h["X-MyCustomHeader"] = e.Message)
            .WithBody((req,sw, exception) =>
                {
                    byte[] array = Encoding.UTF8.GetBytes(exception.ToString());
                    return sw.WriteAsync(array, 0, array.Length);
                })
        .NextPolicy();

        // Ensure that all exception types are handled by adding handler for generic exception at the end.
        options.For<Exception>()
        .Log(lo =>
            {
                lo.EventIdFactory = (c, e) => new EventId(123, "UnhandlerException");
                lo.Category = (context, exception) => "MyCategory";
            })
        .Response(null, ResponseAlreadyStartedBehaviour.GoToNextHandler)
            .ClearCacheHeaders()
            .WithObjectResult((r, e) => new { msg = e.Message, path = r.Path })
        .Handled();
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseExceptionHandlingPolicies();
    app.UseMvc();
}
Run Code Online (Sandbox Code Playgroud)


Arj*_*jun 17

很好的答案对我有很大的帮助,但我想在我的中间件中传递HttpStatusCode来管理运行时的错误状态代码.

根据这个链接我有一些想法做同样的事情.所以我将Andrei Answer与此合并.所以我的最终代码如下:
1.基类

public class ErrorDetails
{
    public int StatusCode { get; set; }
    public string Message { get; set; }

    public override string ToString()
    {
        return JsonConvert.SerializeObject(this);
    }
}
Run Code Online (Sandbox Code Playgroud)

2.自定义异常类类型

 public class HttpStatusCodeException : Exception
{
    public HttpStatusCode StatusCode { get; set; }
    public string ContentType { get; set; } = @"text/plain";

    public HttpStatusCodeException(HttpStatusCode statusCode)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, string message) : base(message)
    {
        this.StatusCode = statusCode;
    }

    public HttpStatusCodeException(HttpStatusCode statusCode, Exception inner) : this(statusCode, inner.ToString()) { }

    public HttpStatusCodeException(HttpStatusCode statusCode, JObject errorObject) : this(statusCode, errorObject.ToString())
    {
        this.ContentType = @"application/json";
    }

}
Run Code Online (Sandbox Code Playgroud)


3.自定义异常中间件

public class CustomExceptionMiddleware
    {
        private readonly RequestDelegate next;

    public CustomExceptionMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context /* other dependencies */)
    {
        try
        {
            await next(context);
        }
        catch (HttpStatusCodeException ex)
        {
            await HandleExceptionAsync(context, ex);
        }
        catch (Exception exceptionObj)
        {
            await HandleExceptionAsync(context, exceptionObj);
        }
    }

    private Task HandleExceptionAsync(HttpContext context, HttpStatusCodeException exception)
    {
        string result = null;
        context.Response.ContentType = "application/json";
        if (exception is HttpStatusCodeException)
        {
            result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)exception.StatusCode }.ToString();
            context.Response.StatusCode = (int)exception.StatusCode;
        }
        else
        {
            result = new ErrorDetails() { Message = "Runtime Error", StatusCode = (int)HttpStatusCode.BadRequest }.ToString();
            context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        }
        return context.Response.WriteAsync(result);
    }

    private Task HandleExceptionAsync(HttpContext context, Exception exception)
    {
        string result = new ErrorDetails() { Message = exception.Message, StatusCode = (int)HttpStatusCode.InternalServerError }.ToString();
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        return context.Response.WriteAsync(result);
    }
}
Run Code Online (Sandbox Code Playgroud)


4.扩展方法

public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app)
    {
        app.UseMiddleware<CustomExceptionMiddleware>();
    }
Run Code Online (Sandbox Code Playgroud)

5.在startup.cs中配置Method

app.ConfigureCustomExceptionMiddleware();
app.UseMvc();
Run Code Online (Sandbox Code Playgroud)

现在我的帐户控制器中的登录方法:

 try
        {
            IRepository<UserMaster> obj = new Repository<UserMaster>(_objHeaderCapture, Constants.Tables.UserMaster);
            var Result = obj.Get().AsQueryable().Where(sb => sb.EmailId.ToLower() == objData.UserName.ToLower() && sb.Password == objData.Password.ToEncrypt() && sb.Status == (int)StatusType.Active).FirstOrDefault();
            if (Result != null)//User Found
                return Result;
            else// Not Found
                throw new HttpStatusCodeException(HttpStatusCode.NotFound, "Please check username or password");
        }
        catch (Exception ex)
        {
            throw ex;
        }
Run Code Online (Sandbox Code Playgroud)

上面你可以看到我是否找不到用户然后提出HttpStatusCodeException,其中我已经通过HttpStatusCode.NotFound状态和自定义消息
在中间件

catch(HttpStatusCodeException ex)

将被调用阻止将控制传递给

private Task HandleExceptionAsync(HttpContext context,HttpStatusCodeException exception)方法

.


但如果我之前遇到运行时错误怎么办?为此,我使用了try catch块抛出异常,并将在catch(Exception exceptionObj)块中捕获并将控制传递给

任务HandleExceptionAsync(HttpContext上下文,异常异常)

方法.

我使用了一个ErrorDetails类来实现一致性.

  • 您不想使用异常,因为它们会减慢您的 api。例外是非常昂贵的。 (3认同)
  • 您确定要使用“throw ex;”吗?而不是“扔”;? (3认同)
  • @Inaie,不能这么说......但似乎你从来没有遇到过任何异常需要处理......干得好 (2认同)

Cou*_*ero 16

首先,感谢Andrei,因为我的解决方案基于他的例子.

我包括我的,因为它是一个更完整的样本,可能会节省读者一些时间.

Andrei的方法的局限在于不处理日志记录,捕获可能有用的请求变量和内容协商(无论客户端请求什么,它总是返回JSON - XML /纯文本等).

我的方法是使用ObjectResult,它允许我们使用烘焙到MVC的功能.

此代码还可以防止缓存响应.

错误响应的修饰方式可以由XML序列化程序序列化.

public class ExceptionHandlerMiddleware
{
    private readonly RequestDelegate next;
    private readonly IActionResultExecutor<ObjectResult> executor;
    private readonly ILogger logger;
    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public ExceptionHandlerMiddleware(RequestDelegate next, IActionResultExecutor<ObjectResult> executor, ILoggerFactory loggerFactory)
    {
        this.next = next;
        this.executor = executor;
        logger = loggerFactory.CreateLogger<ExceptionHandlerMiddleware>();
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await next(context);
        }
        catch (Exception ex)
        {
            logger.LogError(ex, $"An unhandled exception has occurred while executing the request. Url: {context.Request.GetDisplayUrl()}. Request Data: " + GetRequestData(context));

            if (context.Response.HasStarted)
            {
                throw;
            }

            var routeData = context.GetRouteData() ?? new RouteData();

            ClearCacheHeaders(context.Response);

            var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

            var result = new ObjectResult(new ErrorResponse("Error processing request. Server error."))
            {
                StatusCode = (int) HttpStatusCode.InternalServerError,
            };

            await executor.ExecuteAsync(actionContext, result);
        }
    }

    private static string GetRequestData(HttpContext context)
    {
        var sb = new StringBuilder();

        if (context.Request.HasFormContentType && context.Request.Form.Any())
        {
            sb.Append("Form variables:");
            foreach (var x in context.Request.Form)
            {
                sb.AppendFormat("Key={0}, Value={1}<br/>", x.Key, x.Value);
            }
        }

        sb.AppendLine("Method: " + context.Request.Method);

        return sb.ToString();
    }

    private static void ClearCacheHeaders(HttpResponse response)
    {
        response.Headers[HeaderNames.CacheControl] = "no-cache";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
        response.Headers.Remove(HeaderNames.ETag);
    }

    [DataContract(Name= "ErrorResponse")]
    public class ErrorResponse
    {
        [DataMember(Name = "Message")]
        public string Message { get; set; }

        public ErrorResponse(string message)
        {
            Message = message;
        }
    }
}
Run Code Online (Sandbox Code Playgroud)


Edw*_*rey 9

首先,配置ASP.NET Core 2 Startup以重新执行错误页面,以查找来自Web服务器的任何错误以及任何未处理的异常.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment()) {
        // Debug config here...
    } else {
        app.UseStatusCodePagesWithReExecute("/Error");
        app.UseExceptionHandler("/Error");
    }
    // More config...
}
Run Code Online (Sandbox Code Playgroud)

接下来,定义一个异常类型,使您可以使用HTTP状态代码抛出错误.

public class HttpException : Exception
{
    public HttpException(HttpStatusCode statusCode) { StatusCode = statusCode; }
    public HttpStatusCode StatusCode { get; private set; }
}
Run Code Online (Sandbox Code Playgroud)

最后,在错误页面的控制器中,根据错误原因以及最终用户是否直接看到响应来自定义响应.此代码假定所有API URL都以/api/.

[AllowAnonymous]
public IActionResult Error()
{
    // Gets the status code from the exception or web server.
    var statusCode = HttpContext.Features.Get<IExceptionHandlerFeature>()?.Error is HttpException httpEx ?
        httpEx.StatusCode : (HttpStatusCode)Response.StatusCode;

    // For API errors, responds with just the status code (no page).
    if (HttpContext.Features.Get<IHttpRequestFeature>().RawTarget.StartsWith("/api/", StringComparison.Ordinal))
        return StatusCode((int)statusCode);

    // Creates a view model for a user-friendly error page.
    string text = null;
    switch (statusCode) {
        case HttpStatusCode.NotFound: text = "Page not found."; break;
        // Add more as desired.
    }
    return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier, ErrorText = text });
}
Run Code Online (Sandbox Code Playgroud)

ASP.NET Core将记录您要调试的错误详细信息,因此您可能希望向(可能不受信任的)请求者提供状态代码.如果您想显示更多信息,可以增强HttpException提供它.对于API的错误,您可以通过更换放JSON编码错误信息在邮件正文中return StatusCode...使用return Json....


Ale*_*aus 6

以下是Microsoft 的官方指南,涵盖所有 .NET 版本的 WebAPI 和 MVC 案例。

对于 Web API,它建议重定向到专用控制器端点以返回ProblemDetails。由于它可能会导致OpenAPI 规范中不应直接调用的端点潜在暴露,因此我建议采用一个更简单的解决方案:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...

    app.UseExceptionHandler(a => a.Run(async context =>
    {
        var error = context.Features.Get<IExceptionHandlerFeature>().Error;
        var problem = new ProblemDetails { Title = "Critical Error"};
        if (error != null)
        {
            if (env.IsDevelopment())
            {
                problem.Title = error.Message;
                problem.Detail = error.StackTrace;
            }
            else
                problem.Detail = error.Message;
        }
        await context.Response.WriteAsJsonAsync(problem);
    }));
    ...
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,我们利用标准中间件返回自定义详细信息(带有开发模式的堆栈跟踪)并避免创建“内部”端点。

PS 请注意,官方指南依赖IExceptionHandlerPathFeature于 .NET v3 之前和此后(目前直到 v5) - IExceptionHandlerFeature.

PSS 如果您从 Domain 层抛出异常以将其转换为 4xx 代码,我建议使用khellang 的 ProblemDetailsMiddleware或返回DomainResult,稍后可以将其转换为IActionResultIResult。后一个选项可帮助您获得相同的结果,而不会产生异常开销。

  • 我喜欢这个,因为它很简单并且似乎很有效——只需添加上面的代码,您就拥有了一个即时的全局异常处理程序。注意:如果您正在使用“app.UseDeveloperExceptionPage()”,请不要忘记将其删除,以便此解决方案和类似的解决方案发挥作用。 (2认同)
  • 刚刚测试了它,它确实处理了从其他线程抛出的异常(我检查了“Thread.CurrentThread.ManagedThreadId”以获取此声明)。您的情况更有可能有其他原因(例如异常映射中间件)。另外,请注意[SO post](/sf/answers/4647193841/)中强调的中间件的注册顺序。 (2认同)