系列導航
- 使用.NET 6開發TodoList應用文章索引
需求
因為在專案中,會有各種各樣的領域例外或系統例外被拋出來,那么在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 = https://www.cnblogs.com/code4nothing/archive/2021/12/27/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請求,
參考資料
- Write custom ASP.NET Core middleware
- Filters in ASP.NET Core
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/393830.html
標籤:.NET技术
