ASP.NET MVC 中的過濾器(Filter)是 AOP(面向切面編程) 思想的一種實現,供我們在執行管道的特定階段執行代碼,通過使用過濾器可以實現 短路請求、緩存請求結果、日誌統一記錄、參數合法性驗證、異常統一處理、返回值格式化 等等,同時使業務代碼更加簡潔單純,避免很多重複代碼。
過濾器執行流程
MVC 在選擇要執行的 Action 方法後,會執行過濾器管道,流程如下圖:
過濾器作用域
過濾器作用域設置是非常靈活的,可以選擇爲:
全局有效(整個 MVC 應用程序下的每一個 Action);
僅對某些 Controller 有效 (控制器內所有的 Action );
僅對某些 Action 有效;
過濾器類型
過濾器分爲 Authorization Filter、Resource Filter、Action Filter、Exception Filter、Result Filter,每種過濾器都有其應用場景,不同類型的過濾器會在執行管道的不同階段運行,我們需要根據實際情況來選擇使用。
Authorization Filter
授權過濾器在過濾器管道中第一個被執行,通常用於驗證請求的合法性(通過實現接口 IAuthorizationFilter or IAsyncAuthorizationFilter)
Resource Filter
資源過濾器在過濾器管道中第二個被執行,通常用於請求結果的緩存和短路過濾器管道(通過實現接口 IResourceFilter or IAsyncResourceFilter)
Action Filter
Acioin 過濾器可設置在調用 Acioin 方法之前和之後執行代碼,如:請求參數的驗證(通過實現接口 IActionFilter or IAsyncActionFilter)
Exception Filter
程序異常信息處理(通過實現接口 IExceptionFilter or IAsyncExceptionFilter)
Result Filter
Action 執行完成後執行,對執行結果格式化處理(通過實現接口 IResultFilter or IAsyncResultFilter)
public class AddHeaderResultFilter : IResultFilter
{
public void OnResultExecuted(ResultExecutedContext context)
{
Console.WriteLine("AddHeaderResultFilter:OnResultExecuted");
}
public void OnResultExecuting(ResultExecutingContext context)
{
context.HttpContext.Response.Headers.Add("ResultFilter", new string[] { "AddHeader" });
context.Result = new ObjectResult(new ApiResult<string>()
{
Code = Enum.ResultCode.Success,
Data = "我是 AddHeaderResultFilter 修改後的值"
});
}
}
Filter Attributes
將過濾器接口的實現以特性(Attributes) 的方式使用是非常方便的,過濾器特性可應用於 Controller 和 Action 。框架包含了內置的基於特性的過濾器,我們可以直接繼承或另外定製。
繼承內置的 ExceptionFilterAttribute:
public class CustomExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
context.Result = new ObjectResult(new ApiResult()
{
Code = Enum.ResultCode.Exception,
ErrorMessage = $"CustomExceptionFilterAttribute: {context.Exception.Message}"
});
}
}
[CustomExceptionFilter]
public ApiResult ExceptionAttributeTest()
{
throw new Exception("Boom");
}
定製 ResourceFilterAttribute:
public class ShortCircuitingResourceFilterAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
public void OnResourceExecuting(ResourceExecutingContext context)
{
context.Result = new ObjectResult(new ApiResult<string>()
{
Code = Enum.ResultCode.Failed,
Data = "我是 ShortCircuitingResourceFilterAttribute 的返回值"
});
}
}
過濾器實戰
問題:在開發中都存在這樣的場景,方法參數需要添加一些 if 的判斷,不合法直接返回錯誤信息,方法體加個 try\catch,捕獲到異常後記錄日誌,而這些基本是每一個接口方法必須的,實際情況下我們可能是 ctrl+c & ctrl+v ,部分代碼微調,搞定,並不會花什麼時間,但我們可以看看整個文件的代碼,大部分都是一樣的架子,頭部驗證,尾部捕獲,中間調用主邏輯層的方法,滿滿的重複代碼,而且如果後期要調整基礎架子,每個方法都得來一遍,那必須得蛋疼死。
解決方案: Action Filter + Exception Filter
- 創建一個 Filter,同時實現 IAsyncActionFilter 和 IAsyncExceptionFilter:
public class XXXActionFilter : IAsyncActionFilter, IAsyncExceptionFilter { private IDictionary<string, object> _actionArguments; public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // 如果參數驗證不通過,返回參數錯誤的信息 if (!context.ModelState.IsValid) { var errorMessages = new List<string>(); foreach (var key in context.ModelState.Keys) { var state = context.ModelState[key]; var errorModel = state?.Errors?.First(); if (errorModel != null) errorMessages.Add($"{key}:{errorModel.ErrorMessage}"); } var result = new ApiResult() { Code = ResultCode.ArgumentError, Message = errorMessages.Join(",") }; context.Result = new ObjectResult(result); return; } // 保存下來,ExceptionContext 中取不到,如果出異常了需要記錄請求參數 _actionArguments = context.ActionArguments; await next(); } public async Task OnExceptionAsync(ExceptionContext context) { // 記錄異常日誌 // some code.......... var result = new ApiResult() { Code = ResultCode.Exception, Message = $"{context.ActionDescriptor.RouteValues["action"]} Exception") }; context.Result = new ObjectResult(result); await Task.CompletedTask; } }
- Startup.cs 的 ConfigureServices 中添加 XXXActionFilter 爲全局有效:
services.AddMvc(options => { options.Filters.Add<XXXActionFilter>(); });
- Controller 中的添加測試方法 :
public ApiResult ActionTest(XXXRequest request) { if (request.Id == "1") { throw new Exception("xxxx"); } return new ApiResult<string>() { Code = Enum.ResultCode.Success, Data = "ActionTest" }; }
public class XXXRequest { [Required] public string Id { get; set; } }
調用 ActionTest 進行測試,如果 id 沒傳,會進去 OnActionExecutionAsync 的 context.ModelState.IsValid 爲 fasle 的情況:
如果 id 爲1,會進入 OnExceptionAsync:
其他情況下正常返回: