dotNet8 全域性異常處理

董瑞鹏發表於2024-03-15

前言

異常的處理在我們應用程式中是至關重要的,在 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

builder.Services.AddProblemDetails();

如果我們不對異常進行捕獲處理,Asp.Net Core 提供了兩種不同的內建集中式機制來處理未經處理的異常

  • app.UseDeveloperExceptionPage();

    開發人員異常中介軟體

    開發人員異常中介軟體會顯示伺服器錯誤的詳細堆疊跟蹤,不建議在非開發環境顯示,暴漏核心錯誤資訊給客戶端,有嚴重的安全風險

  • app.UseExceptionHandler(); 異常處理程式中介軟體,
    使用異常處理程式中介軟體生成的是標準的簡化回覆

測試 UseDeveloperExceptionPage

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 介面
回參

{
  "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();

異常處理程式中介軟體

// app.UseDeveloperExceptionPage();// 開發人員異常頁
app.UseExceptionHandler();//異常處理中介軟體

測試一下

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
  "title": "An error occurred while processing your request.",
  "status": 500
}

可以看到只保留了最基本的報錯資訊,這樣第一步我們已經完成了。

自定義異常 和 IExceptionHandler

建立一個自定義異常資訊

public class CustomException(int code, string message) : Exception(message)
{
    public int Code { get; private set; } = code;

    public string Message { get; private set; } = message;
}

整合IExceptionHandler建立自定義異常處理器

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 處理系統異常

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

builder.Services.AddExceptionHandler<CustomExceptionHandler>();
builder.Services.AddExceptionHandler<SystemExceptionHandle>();

新加介面測試一下

app.MapGet("/CustomThrow", () =>
{
    throw new CustomException(StatusCodes.Status403Forbidden, "你沒有許可權!");
}).WithOpenApi();

回參

{
  "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 新的異常處理方式,當時也可以用UseExceptionHandlerlambda方式可以建立,但是不如這種強型別約束的規範,大家在升級 dotNet8 時可以參考本文來修改專案現有的全部異常捕獲方式。

Demo 原始碼

dotNet 官網教程

相關文章