前言
异常的处理在我们应用程序中是至关重要的,在 dotNet 中有很多异常处理的机制,比如MVC的异常筛选器, 管道中间件定义try catch捕获异常处理亦或者第三方的解决方案Hellang.Middleware.ProblemDetails等。MVC异常筛选器不太灵活,对管道的部分异常捕获不到,后两种方式大家项目应该经常出现。
在 dotNet8 发布之后支持了新的异常处理机制 IExceptionHandler或者UseExceptionHandler异常处理程序的lambda配置,配合dotNet7原生支持的ProblemDetail使得异常处理更加规范。
本文用一个简单的 Demo 带大家看一下新的异常处理方式
文末有示例完整的源代码
先起一个WebApi的新项目
Problem Details
Problem Details是一种在HTTP API中用于描述错误信息的标准化格式。根据 RFC 7807,Problem Details 提供了一种统一、可机器读取的方式来呈现出发生在 API 请求中的问题。它包括各种属性,如 title、status、detail、type 等,用于清晰地描述错误的性质和原因。通过使用 Problem Details,开发人员可以为 API 的错误响应提供一致性和易于理解的结构化格式,从而帮助客户端更好地处理和解决问题。
项目中使用 Problem Details
            
            
              c#
              
              
            
          
          builder.Services.AddProblemDetails();
        如果我们不对异常进行捕获处理,Asp.Net Core 提供了两种不同的内置集中式机制来处理未经处理的异常
- 
app.UseDeveloperExceptionPage();
开发人员异常中间件
开发人员异常中间件会显示服务器错误的详细堆栈跟踪,不建议在非开发环境显示,暴漏核心错误信息给客户端,有严重的安全风险
 - 
app.UseExceptionHandler(); 异常处理程序中间件,
使用异常处理程序中间件生成的是标准的简化回复
 
测试 UseDeveloperExceptionPage
            
            
              c#
              
              
            
          
          var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddProblemDetails();
var app = builder.Build();
app.MapGet("/TestUseDeveloperExceptionPage",
    () => { throw new Exception("测试UseDeveloperExceptionPage"); });
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
    app.UseDeveloperExceptionPage();// 开发人员异常页
}
app.UseStatusCodePages();
app.UseHttpsRedirection();
app.Run();
        调用 TestUseDeveloperExceptionPage 接口
回参
            
            
              json
              
              
            
          
          {
  "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
  "title": "System.Exception",
  "status": 500,
  "detail": "测试UseDeveloperExceptionPage",
  "exception": {
    "details": "System.Exception: 测试UseDeveloperExceptionPage\r\n   at Program.<>c.<<Main>$>b__0_0() in C:\\dotNetParadise\\dot-net-paradise-exception\\dotNetParadise-Exception\\dotNetParadise-Exception\\Program.cs:line 7\r\n   at lambda_method3(Closure, Object, HttpContext)\r\n   at Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware.Invoke(HttpContext context)\r\n   at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)\r\n   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)",
    "headers": {
      "Accept": [
        "*/*"
      ],
      "Host": [
        "localhost:7130"
      ],
      "User-Agent": [
        "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0"
      ],
      "Accept-Encoding": [
        "gzip, deflate, br"
      ],
      "Accept-Language": [
        "en-US,en;q=0.9"
      ],
      "Cookie": [
        "ajs_anonymous_id=b96604ea-c096-4693-acfb-b3a9e8403f0e; Quasar_admin_Vue3_username=admin; Quasar_admin_Vue3_token=b1aa15b6-02bb-44b9-8668-0157a1d9b6f0; Quasar_admin_Vue3_lang=en-US"
      ],
      "Referer": [
        "https://localhost:7130/swagger/index.html"
      ],
      "sec-ch-ua": [
        "\"Chromium\";v=\"122\", \"Not(A:Brand\";v=\"24\", \"Microsoft Edge\";v=\"122\""
      ],
      "sec-ch-ua-mobile": [
        "?0"
      ],
      "sec-ch-ua-platform": [
        "\"Windows\""
      ],
      "sec-fetch-site": [
        "same-origin"
      ],
      "sec-fetch-mode": [
        "cors"
      ],
      "sec-fetch-dest": [
        "empty"
      ]
    },
    "path": "/TestUseDeveloperExceptionPage",
    "endpoint": "HTTP: GET /TestUseDeveloperExceptionPage",
    "routeValues": {}
  }
        可以看到所有的信息都抛出来给到了客户端,适合在开发环境用,非开发环境尤其是生产环境不要启用。
app.UseExceptionHandler();
异常处理程序中间件
            
            
              c#
              
              
            
          
          // app.UseDeveloperExceptionPage();// 开发人员异常页
app.UseExceptionHandler();//异常处理中间件
        测试一下
            
            
              json
              
              
            
          
          {
  "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
  "title": "An error occurred while processing your request.",
  "status": 500
}
        可以看到只保留了最基本的报错信息,这样第一步我们已经完成了。
自定义异常 和 IExceptionHandler
创建一个自定义异常信息
            
            
              c#
              
              
            
          
          public class CustomException(int code, string message) : Exception(message)
{
    public int Code { get; private set; } = code;
    public string Message { get; private set; } = message;
}
        集成IExceptionHandler创建自定义异常处理器
        
            
            
              c#
              
              
            
          
          public class CustomExceptionHandler(ILogger<CustomException> logger, IWebHostEnvironment environment) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
    {
        if (exception is not CustomException customException) return false;
        logger.LogError(
              exception, "Exception occurred: {Message} {StackTrace} {Source}", exception.Message, exception.StackTrace, exception.Source);
        var problemDetails = new ProblemDetails
        {
            Status = customException.Code,
            Title = customException.Message,
        };
        if (environment.IsDevelopment())
        {
            problemDetails.Detail = $"Exception occurred: {customException.Message} {customException.StackTrace} {customException.Source}";
        }
        httpContext.Response.StatusCode = problemDetails.Status.Value;
        await httpContext.Response
            .WriteAsJsonAsync(problemDetails, cancellationToken);
        return true;
    }
}
        可以注册多个自定义异常处理器分别处理不同类型的异常,按默认的注册顺序来处理,如果返回
true则会处理此异常返回false会跳到下一个ExceptionHandler,没处理的异常在 UseExceptionHandler 中间件做最后处理。
创建第二个ExceptionHandler 处理系统异常
            
            
              c#
              
              
            
          
          public class SystemExceptionHandle(ILogger<CustomException> logger, IWebHostEnvironment environment) : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(HttpContext httpContext, Exception exception, CancellationToken cancellationToken)
    {
        if (exception is CustomException) return false;
        logger.LogError(
              exception, "Exception occurred: {Message} {StackTrace} {Source}", exception.Message, exception.StackTrace, exception.Source);
        var problemDetails = new ProblemDetails
        {
            Status = StatusCodes.Status500InternalServerError,
            Title = "An error occurred while processing your request",
        };
        if (environment.IsDevelopment())
        {
            problemDetails.Detail = $"Exception occurred: {exception.Message} {exception.StackTrace} {exception.Source}";
        }
        httpContext.Response.StatusCode = problemDetails.Status.Value;
        await httpContext.Response
            .WriteAsJsonAsync(problemDetails, cancellationToken);
        return true;
    }
}
        IOC 容器注册ExceptionHandler
        
            
            
              c#
              
              
            
          
          builder.Services.AddExceptionHandler<CustomExceptionHandler>();
builder.Services.AddExceptionHandler<SystemExceptionHandle>();
        新加接口测试一下
            
            
              c#
              
              
            
          
          app.MapGet("/CustomThrow", () =>
{
    throw new CustomException(StatusCodes.Status403Forbidden, "你没有权限!");
}).WithOpenApi();
        回参
            
            
              json
              
              
            
          
          {
  "title": "你没有权限!",
  "status": 403,
  "detail": "Exception occurred: 你没有权限!    at Program.<>c.<<Main>$>b__0_1() in C:\\dotNetParadise\\dot-net-paradise-exception\\dotNetParadise-Exception\\dotNetParadise-Exception\\Program.cs:line 15\r\n   at lambda_method5(Closure, Object, HttpContext)\r\n   at Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware.Invoke(HttpContext context)\r\n   at Microsoft.AspNetCore.Diagnostics.StatusCodePagesMiddleware.Invoke(HttpContext context)\r\n   at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddlewareImpl.<Invoke>g__Awaited|10_0(ExceptionHandlerMiddlewareImpl middleware, HttpContext context, Task task) dotNetParadise-Exception"
}
        可以看出全局异常捕获生效了。
最后
本文讲的是 dotNet8 新的异常处理方式,当时也可以用UseExceptionHandler的lambda方式可以创建,但是不如这种强类型约束的规范,大家在升级 dotNet8 时可以参考本文来修改项目现有的全部异常捕获方式。