我使用ASP。NET核心为我的新的REST API项目后使用常规的ASP。NET Web API很多年了。我看不出在ASP中有什么处理异常的好方法。NET核心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
        };
    }
}

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

services.AddMvc(options =>
{
    options.Filters.Add(new AuthorizationFilter());
    options.Filters.Add(new ErrorHandlingFilter());
});

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

那么我如何捕捉所有应用程序异常以及任何异常从动作过滤器?


当前回答

下面是微软的官方指南,涵盖了所有版本的。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);
    }));
    ...
}

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

注:官方指南依赖于。net v3之前的IExceptionHandlerPathFeature,此后(到目前为止v5)依赖于IExceptionHandlerFeature。

p.s.如果你从域层抛出异常,将它们转换为4xx代码,我建议使用khellang的ProblemDetailsMiddleware或返回DomainResult,可以稍后转换为IActionResult或IResult。后一个选项可以帮助您实现相同的结果,而不需要异常的开销。

其他回答

快速和简单的异常处理

简单地在ASP之前添加这个中间件。NET路由到中间件注册。

app.UseExceptionHandler(c => c.Run(async context =>
{
    var exception = context.Features
        .Get<IExceptionHandlerPathFeature>()
        .Error;
    var response = new { error = exception.Message };
    await context.Response.WriteAsJsonAsync(response);
}));
app.UseMvc(); // or .UseRouting() or .UseEndpoints()

完成了!


为日志记录和其他目的启用依赖注入

步骤1。在启动过程中,注册异常处理路由:

// It should be one of your very first registrations
app.UseExceptionHandler("/error"); // Add this
app.UseEndpoints(endpoints => endpoints.MapControllers());

步骤2。创建控制器,处理所有异常并产生错误响应:

[AllowAnonymous]
[ApiExplorerSettings(IgnoreApi = true)]
public class ErrorsController : ControllerBase
{
    [Route("error")]
    public MyErrorResponse Error()
    {
        var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
        var exception = context.Error; // Your exception
        var code = 500; // Internal Server Error by default

        if      (exception is MyNotFoundException) code = 404; // Not Found
        else if (exception is MyUnauthException)   code = 401; // Unauthorized
        else if (exception is MyException)         code = 400; // Bad Request

        Response.StatusCode = code; // You can use HttpStatusCode enum instead

        return new MyErrorResponse(exception); // Your error model
    }
}

一些重要的注意事项和观察:

You can inject your dependencies into the Controller's constructor. [ApiExplorerSettings(IgnoreApi = true)] is needed. Otherwise, it may break your Swashbuckle swagger Again, app.UseExceptionHandler("/error"); has to be one of the very top registrations in your Startup Configure(...) method. It's probably safe to place it at the top of the method. The path in app.UseExceptionHandler("/error") and in controller [Route("error")] should be the same, to allow the controller handle exceptions redirected from exception handler middleware.

这里是微软官方文档的链接。


响应模型思想。

实现您自己的响应模型和异常。 这个例子只是一个很好的起点。每个服务都需要以自己的方式处理异常。使用所描述的方法,您可以完全灵活地处理异常并从服务返回正确的响应。

一个错误响应模型的例子(只是给你一些想法):

public class MyErrorResponse
{
    public string Type { get; set; }
    public string Message { get; set; }
    public string StackTrace { get; set; }

    public MyErrorResponse(Exception ex)
    {
        Type = ex.GetType().Name;
        Message = ex.Message;
        StackTrace = ex.ToString();
    }
}

对于更简单的服务,你可能想要实现http状态码异常,看起来像这样:

public class HttpStatusException : Exception
{
    public HttpStatusCode Status { get; private set; }

    public HttpStatusException(HttpStatusCode status, string msg) : base(msg)
    {
        Status = status;
    }
}

这可以从任何地方抛出:

throw new HttpStatusCodeException(HttpStatusCode.NotFound, "User not found");

然后你的处理代码可以简化成这样:

if (exception is HttpStatusException httpException)
{
    code = (int) httpException.Status;
}

HttpContext功能。Get < IExceptionHandlerFeature >()什么?

ASP.NET Core developers embraced the concept of middlewares where different aspects of functionality such as Auth, MVC, Swagger etc. are separated and executed sequentially in the request processing pipeline. Each middleware has access to request context and can write into the response if needed. Taking exception handling out of MVC makes sense if it's important to handle errors from non-MVC middlewares the same way as MVC exceptions, which I find is very common in real world apps. So because built-in exception handling middleware is not a part of MVC, MVC itself knows nothing about it and vice versa, exception handling middleware doesn't really know where the exception is coming from, besides of course it knows that it happened somewhere down the pipe of request execution. But both may needed to be "connected" with one another. So when exception is not caught anywhere, exception handling middleware catches it and re-runs the pipeline for a route, registered in it. This is how you can "pass" exception handling back to MVC with consistent content negotiation or some other middleware if you wish. The exception itself is extracted from the common middleware context. Looks funny but gets the job done :).

通过添加你自己的“异常处理中间件”,很难重用一些良好的内置异常处理逻辑,比如在错误发生时向客户端发送一个“符合RFC 7807的有效负载”。

我所做的是在Startup.cs类之外扩展内置的异常处理程序,以处理自定义异常或覆盖现有异常的行为。例如,在不改变其他异常默认行为的情况下,将ArgumentException转换为BadRequest:

在Startup.cs上添加:

app.UseExceptionHandler("/error");

并像这样扩展ErrorController.cs:

using System;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;

namespace Api.Controllers
{
    [ApiController]
    [ApiExplorerSettings(IgnoreApi = true)]
    [AllowAnonymous]
    public class ErrorController : ControllerBase
    {
        [Route("/error")]
        public IActionResult Error(
            [FromServices] IWebHostEnvironment webHostEnvironment)
        {
            var context = HttpContext.Features.Get<IExceptionHandlerFeature>();
            var exceptionType = context.Error.GetType();
            
            if (exceptionType == typeof(ArgumentException)
                || exceptionType == typeof(ArgumentNullException)
                || exceptionType == typeof(ArgumentOutOfRangeException))
            {
                if (webHostEnvironment.IsDevelopment())
                {
                    return ValidationProblem(
                        context.Error.StackTrace,
                        title: context.Error.Message);
                }

                return ValidationProblem(context.Error.Message);
            }

            if (exceptionType == typeof(NotFoundException))
            {
                return NotFound(context.Error.Message);
            }

            if (webHostEnvironment.IsDevelopment())
            {
                return Problem(
                    context.Error.StackTrace,
                    title: context.Error.Message
                    );
            }
            
            return Problem();
        }
    }
}

注意:

NotFoundException是一个自定义异常,所有你需要做的是抛出新的NotFoundException(null);或抛出新的ArgumentException(“无效参数。”); 您不应该向客户端提供敏感的错误信息。服务错误是一种安全风险。

在任何特定方法上处理异常的简单方法是:

using Microsoft.AspNetCore.Http;
...

public ActionResult MyAPIMethod()
{
    try
    {
       var myObject = ... something;

       return Json(myObject);
    }
    catch (Exception ex)
    {
        Log.Error($"Error: {ex.Message}");
        return StatusCode(StatusCodes.Status500InternalServerError);
    }         
}

最好的办法是使用中间件来实现您正在寻找的日志记录。您希望将异常日志记录放在一个中间件中,然后在另一个中间件中处理显示给用户的错误页面。这允许逻辑分离,并遵循微软在2个中间件组件上的设计。这里有一个很好的微软文档的链接:ASP中的错误处理。网络核心

对于您的特定示例,您可能希望使用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();
}

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

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

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

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

有一个内置的中间件:

ASP。NET Core 5版本:

app.UseExceptionHandler(a => a.Run(async context =>
{
    var exceptionHandlerPathFeature = context.Features.Get<IExceptionHandlerPathFeature>();
    var exception = exceptionHandlerPathFeature.Error;
    
    await context.Response.WriteAsJsonAsync(new { error = exception.Message });
}));

旧版本(他们没有WriteAsJsonAsync扩展):

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);
}));

它应该做几乎相同的事情,只是要编写的代码少了一点。

重要:记住将它添加在MapControllers \ UseMvc(或。net Core 3中的UseRouting)之前,因为顺序很重要。