前言
異常的處理在我們應用程式中是至關重要的,在 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 新的異常處理方式,當時也可以用UseExceptionHandler
的lambda
方式可以建立,但是不如這種強型別約束的規範,大家在升級 dotNet8 時可以參考本文來修改專案現有的全部異常捕獲方式。
Demo 原始碼
dotNet 官網教程