发布于 2026-01-06 3 阅读
0

每个 ASP.NET Core Web API 项目都需要什么 - 第 3 部分 - 异常处理中间件

每个 ASP.NET Core Web API 项目都需要什么 - 第 3 部分 - 异常处理中间件

上一篇文章中,我介绍了 API 版本控制以及如何将 Swagger 添加到支持 API 版本控制的示例项目中。在本文中,我将展示如何添加自定义中间件来全局处理异常,并在发生错误时创建自定义响应。

谁能写出完全没有bug的代码?反正我写不出来。虽然每个系统都可能出现未处理的异常,但捕获错误、记录并修复它们,以及向客户端提供正确的响应至关重要。异常处理中间件可以帮助我们在同一个地方捕获异常,避免在应用程序中编写重复的异常处理代码。

步骤 1 - 实现异常处理中间件

首先,在Infrastructure文件夹中添加一个新文件夹并命名,Middlewares然后在其中添加一个新文件ApiExceptionHandlingMiddleware.cs。添加以下代码:

public class ApiExceptionHandlingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<ApiExceptionHandlingMiddleware> _logger;

    public ApiExceptionHandlingMiddleware(RequestDelegate next, ILogger<ApiExceptionHandlingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (Exception ex)
        {
            await HandleExceptionAsync(context, ex);
        }
    }

    private async Task HandleExceptionAsync(HttpContext context, Exception ex)
    {
        _logger.LogError(ex, $"An unhandled exception has occurred, {ex.Message}");

        var problemDetails = new ProblemDetails
        {
            Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
            Title = "Internal Server Error",
            Status = (int)HttpStatusCode.InternalServerError,
            Instance = context.Request.Path,
            Detail = "Internal server error occured!"
        };

        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        var result = JsonSerializer.Serialize(problemDetails);

        context.Response.ContentType = "application/json";
        await context.Response.WriteAsync(result);
    }
}
Enter fullscreen mode Exit fullscreen mode

除了将响应状态码设置为 500 之外,响应正文中context.Response.StatusCode = (int)HttpStatusCode.InternalServerError还包含一条格式为 exist 的消息。ProblemDetails

在 ASP.NET Core 2.2 之前,HTTP 400 的默认响应类型BadRequest(ModelState)是:

{
  "": [
    "A non-empty request body is required."
  ]
}
Enter fullscreen mode Exit fullscreen mode

根据互联网工程任务组 (IETF) RFC-7231 文档,ASP.NET Core 团队实现了ProblemDetails,这是一种机器可读格式,用于指定 Web API 响应中的错误,并且符合RFC 7807规范。

步骤 2 - 注册中间件

  • 创建用于注册中间件的扩展方法:
public static class MiddlewareExtensions
{
    public static IApplicationBuilder UseApiExceptionHandling(this IApplicationBuilder app)
        => app.UseMiddleware<ApiExceptionHandlingMiddleware>();
}
Enter fullscreen mode Exit fullscreen mode
  • 打开Startup.cs类,并在Configure方法中添加中间件:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        ...
    }

    app.UseApiExceptionHandling();
Enter fullscreen mode Exit fullscreen mode

如您所知,添加中间件组件的顺序很重要。如果您是UseDeveloperExceptionPage在开发环境中使用,那么请ApiExceptionHandling在完成上述步骤后再添加中间件。

步骤 3 - 将业务错误转化为领域异常

关于何时抛出异常有很多争论,但是,究竟何时应该抛出异常呢?

  • 首要原因是完成整个过程并给出结果是不可能的(快速失败):
private async Task AddProductToBasketAsync(Guid productId)
{
    var product = await _repository.GetProductByIdAsync(productId);
    if(product == null)
        throw new DomainException($"Product with id {productId} could not be found."); 
        // Or simply return null?
        // Or return an error code or warping response into another object that has `Succeeded` property like `IdentityResult`?
        // Or return a tuple (false, "Product with id {productId} could not be found.")?
Enter fullscreen mode Exit fullscreen mode
  • 抛出异常的缺点之一是它会带来性能开销。如果您正在编写高性能应用程序,抛出异常可能会降低性能。

    重申一遍:例外情况必须是真正意义上的例外。
  • 使用错误代码或包装对象有哪些缺点?

    • 抛出异常更安全,因为调用代码可能忘记检查结果是否为错误代码或空值,而继续执行。必须从下往上检查方法调用的结果。
    • 如果将结果封装到另一个对象中,例如`IdentityResult`,则需要支付额外的堆内存分配费用。每次调用都需要初始化一个额外的对象,即使操作成功也是如此。如果使用不同的输入调用 API 100 次,可能会抛出多少次异常?因此,在需要初始化额外对象(以及进行堆内存分配)的情况下,抛出异常的概率并不相同。

步骤 4 - 添加 DomainException 类

  • 在项目根目录创建一个新文件夹并命名,Domain然后添加另一个文件夹Exception
  • DomainException.cs向文件夹添加新文件Exception
public class DomainException : Exception
{
    public DomainException(string message)
        : base(message)
    {
    }
}
Enter fullscreen mode Exit fullscreen mode
  • DomainException在异常处理中间件中捕获并转换为错误请求结果:
private async Task HandleExceptionAsync(HttpContext context, Exception ex)
{
    string result;

    **if (ex is DomainException)
    {
        var problemDetails = new ValidationProblemDetails(new Dictionary<string, string[]> { { "Error", new[] { ex.Message } } })
        {
            Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1",
            Title = "One or more validation errors occurred.",
            Status = (int)HttpStatusCode.BadRequest,
            Instance = context.Request.Path,
        };
        context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
        result = JsonSerializer.Serialize(problemDetails);
    }**
    else
    {
        _logger.LogError(ex, $"An unhandled exception has occurred, {ex.Message}");
        var problemDetails = new ProblemDetails
        {
            Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1",
            Title = "Internal Server Error.",
            Status = (int)HttpStatusCode.InternalServerError,
            Instance = context.Request.Path,
            Detail = "Internal Server Error!"
        };
        context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
        result = JsonSerializer.Serialize(problemDetails);
    }

    context.Response.ContentType = "application/json";
    await context.Response.WriteAsync(result);
}
Enter fullscreen mode Exit fullscreen mode

我把它翻译DomainException成了ValidationProblemDetails类似未处理异常的形式。DomainException稍后我会用到。让我们测试一下域异常的实际应用:

[HttpGet("throw-domain-exception")]
public IActionResult ThrowDomainError()
{
    throw new DomainException("Product could not be found");
}
Enter fullscreen mode Exit fullscreen mode

替代文字

您可以在Github上找到本教程的源代码

文章来源:https://dev.to/moesmp/what-every-asp-net-core-web-api-project-needs-part-3-exception-handling-middleware-3nif