系列導航
需求
因為在項目中,會有各種各樣的領域異常或系統異常被拋出來,那么在Controller里就需要進行完整的try-catch捕獲,並根據是否有異常拋出重新包裝返回值。這是一項機械且繁瑣的工作。有沒有辦法讓框架自己去做這件事呢?
有的,解決方案的名稱叫做全局異常處理,或者叫做如何讓接口優雅地失敗。
目標
我們希望將異常處理和消息返回放到框架中進行統一處理,擺脫Controller層的try-catch塊。
原理和思路
一般而言用來實現全局異常處理的思路有兩種,但是出發點都是通過.NET Web API的管道中間件Middleware Pipeline實現的。第一種方式是通過.NET內建的中間件來實現;第二種是完全自定義中間件實現。
我們會簡單地介紹一下如何通過內建中間件實現,然后實際使用第二種方式來實現我們的代碼,大家可以比較一下異同。
在Api項目中創建Models文件夾並創建ErrorResponse類。
ErrorResponse.cs
using System.Net;
using System.Text.Json;
namespace TodoList.Api.Models;
public class ErrorResponse
{
public HttpStatusCode StatusCode { get; set; } = HttpStatusCode.InternalServerError;
public string Message { get; set; } = "An unexpected error occurred.";
public string ToJsonString() => JsonSerializer.Serialize(this);
}
創建Extensions文件夾並新建一個靜態類ExceptionMiddlewareExtensions實現一個靜態擴展方法:
ExceptionMiddlewareExtensions.cs
using System.Net;
using Microsoft.AspNetCore.Diagnostics;
using TodoList.Api.Models;
namespace TodoList.Api.Extensions;
public static class ExceptionMiddlewareExtensions
{
public static void UseGlobalExceptionHandler(this WebApplication app)
{
app.UseExceptionHandler(appError =>
{
appError.Run(async context =>
{
context.Response.ContentType = "application/json";
var errorFeature = context.Features.Get<IExceptionHandlerFeature>();
if (errorFeature != null)
{
await context.Response.WriteAsync(new ErrorResponse
{
StatusCode = (HttpStatusCode)context.Response.StatusCode,
Message = errorFeature.Error.Message
}.ToJsonString());
}
});
});
}
}
在中間件配置的最開始配置好,注意中間件管道是有順序的,把全局異常處理放到第一步(同時也是請求返回的最后一步)能確保它能攔截到所有可能發生的異常。即這個位置:
var app = builder.Build();
app.UseGlobalExceptionHandler();
就可以實現全局異常處理了。接下來我們看如何完全自定義一個全局異常處理的中間件,其實原理是完全一樣的,只不過我更偏向自定義中間件的代碼組織方式,更加簡潔和一目了然。
與此同時,我們希望對返回值進行格式上的統一包裝,於是定義了這樣的返回類型:
ApiResponse.cs
using System.Text.Json;
namespace TodoList.Api.Models;
public class ApiResponse<T>
{
public T Data { get; set; }
public bool Succeeded { get; set; }
public string Message { get; set; }
public static ApiResponse<T> Fail(string errorMessage) => new() { Succeeded = false, Message = errorMessage };
public static ApiResponse<T> Success(T data) => new() { Succeeded = true, Data = data };
public string ToJsonString() => JsonSerializer.Serialize(this);
}
實現
在Api項目中新建Middlewares文件夾並新建中間件GlobalExceptionMiddleware
GlobalExceptionMiddleware.cs
using System.Net;
using TodoList.Api.Models;
namespace TodoList.Api.Middlewares;
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
public GlobalExceptionMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception exception)
{
// 你可以在這里進行相關的日志記錄
await HandleExceptionAsync(context, exception);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
context.Response.ContentType = "application/json";
context.Response.StatusCode = exception switch
{
ApplicationException => (int)HttpStatusCode.BadRequest,
KeyNotFoundException => (int)HttpStatusCode.NotFound,
_ => (int)HttpStatusCode.InternalServerError
};
var responseModel = ApiResponse<string>.Fail(exception.Message);
await context.Response.WriteAsync(responseModel.ToJsonString());
}
}
這樣我們的ExceptionMiddlewareExtensions就可以寫成下面這樣了:
ExceptionMiddlewareExtensions.cs
using TodoList.Api.Middlewares;
namespace TodoList.Api.Extensions;
public static class ExceptionMiddlewareExtensions
{
public static WebApplication UseGlobalExceptionHandler(this WebApplication app)
{
app.UseMiddleware<GlobalExceptionMiddleware>();
return app;
}
}
驗證
首先我們需要在Controller中包裝我們的返回值,舉一個CreateTodoList的例子,其他的類似修改:
TodoListController.cs
[HttpPost]
public async Task<ApiResponse<Domain.Entities.TodoList>> Create([FromBody] CreateTodoListCommand command)
{
return ApiResponse<Domain.Entities.TodoList>.Success(await _mediator.Send(command));
}
還記得我們在TodoList的領域實體上有一個Colour的屬性嗎,它是一個值對象,並且在賦值的過程中我們讓它有機會拋出一個UnsupportedColourException,我們就用這個領域異常來驗證全局異常處理。
為了驗證需要,我們可以對CreateTodoListCommand做一些修改,讓它接受一個Colour的字符串,相應修改如下:
CreateTodoListCommand.cs
public class CreateTodoListCommand : IRequest<Domain.Entities.TodoList>
{
public string? Title { get; set; }
public string? Colour { get; set; }
}
// 以下代碼位於對應的Handler中,省略其他...
var entity = new Domain.Entities.TodoList
{
Title = request.Title,
Colour = Colour.From(request.Colour ?? string.Empty)
};
啟動Api項目,我們試圖以一個不支持的顏色來創建TodoList:
-
請求

-
響應

順便去看下正常返回的格式是否按我們預期的返回,下面是請求所有TodoList集合的接口返回:

可以看到正常和異常的返回類型已經統一了。
總結
其實實現全局異常處理還有一種方法是通過Filter來做,具體方法可以參考這篇文章:Filters in ASP.NET Core,我們之所以不選擇Filter而使用Middleware主要是基於簡單、易懂,並且作為中間件管道的第一個個中間件加入,有效地覆蓋包括中間件在內的所有組件處理過程。Filter的位置是在路由中間件作用之后才被調用到。實際使用中,兩種方式都有應用。
下一篇我們來實現PUT請求。
