# ASP.NET Core - Filter Filter 可以 在Action `執行前` 和 `執行後` 對 Request 進行加工處理,包括錯誤處理、快取、授權等等。使用 Filter 的好處是可以避免重複的程式碼,例如,錯誤處理可以透過 Filter 合併處理錯誤。 <!-- more --> ## Filter 的運作方式 ![Filter](https://andrewlock.net/content/images/2016/11/Middleware-and-filters-1.svg) Filter 會在 Middleware 執行完後執行。Filter 和 Middleware 都可以達到類似的目的,要使用哪個取決於是否需要存取 MVC context。 Middleware 只能存取 HttpContext,而 Filter 可以存取整個 MVC context,所以 Filter 可以存取 Routing data 和 model binding 的資訊。 一般來說,如果關注的點是和 MVC 無關的則可以使用 Middleware,若是需要驗證或是修改值則要使用 Filter 才有辦法進行存取。 關於 Middleware 的更多資訊請參考 [ASP.NET Core - Middleware](https://tienyulin.github.io/asp-net-core-middleware/)。 ## Filter的類型 Filter 總共有以下幾種不同的類型,會依照順序執行 * Authorization Filter : 最先執行,驗證 Request 是否合法,若不合法則會直接回傳驗證失敗或`401`之類的,若是要經過邏輯運算驗證的則不能在此驗證,需使用 ActionFilter,待邏輯運算回傳後再處理。 * Resource Filter : 在 Authorization Filter 和其他所有 Filter 都執行完之後會執行,用來處理快取或 Model Binding,如果 Cache 中已經有需要的值那就不需要再繼續執行剩下的步驟了,也可以限制檔案上傳大小等等。 * Action Filter : 在 Action 前後執行,處理傳入的 Action 的參數和回應的結果。 * Exception Filter : 處理應用程式沒有處理的例外狀況。 * Result Filter : 應用程式執行無誤後才會執行,若在 Authorization、Resource 或是 Exception 的 Filter 被攔截回傳的話則不會執行 Result Filter ![Filter執行順序](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters/_static/filter-pipeline-2.png?view=aspnetcore-3.1) ## 實作 Filter 以下以 Dotnet Core 3.1 實作,分別繼承不同類型的 Filter 的 Interface 去實作,範例皆為非同步的方法。 await next() 前代表執行前的處理,呼叫 await next() 代表呼叫執行下一個 Filter,await next() 後的則代表執行後的處理。 另外需特別注意的是如果在執行前的處理已經指派值給 context.Result,則不能再呼叫 await next()。 若要使用同步方法,則繼承的 Interface 去除 Async 即可,而同步的實作分為兩個 function,分別為 OnExecuting (執行前)和 OnExecuted (執行後)。 ### Authorization Filter 實作 `IAsyncAuthorizationFilter` 或 `IAuthorizationFilter` 範例: 以 cookie 中帶 token 傳入驗證為例,若 token 驗證不過則回傳 401 未授權 ```csharp= /// <summary> /// Class AuthorizationFilter /// </summary> /// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.IAsyncAuthorizationFilter" /> public class AuthorizationFilter : IAsyncAuthorizationFilter { /// <summary> /// Called early in the filter pipeline to confirm request is authorized. /// </summary> /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext" />.</param> public async Task OnAuthorizationAsync(AuthorizationFilterContext context) { var cookies = context.HttpContext.Request.Cookies; cookies.TryGetValue("token", out string token); if (token.Equals("123456")) { var response = new FailResultViewModel { CorrelationId = Guid.NewGuid().ToString(), Method = $"{context.HttpContext.Request.Path}.{context.HttpContext.Request.Method}", Status = "UnAuthorized", Version = "1.0", Error = new FailInformation() { Domain = "ProjectName", Message = "未授權", Description = "授權驗證失敗" } }; context.Result = new ObjectResult(response) { // 401 StatusCode = (int)HttpStatusCode.Unauthorized }; } } } ``` ### Resource Filter 實作 `IAsyncResourceFilter` 或 `IResourceFilter` 範例 : 以 Cahce 為例,當傳入相同的 Request 時直接從 Cache 回傳結果 ```csharp= /// <summary> /// Class CacheResourceFilter /// </summary> /// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.IAsyncResourceFilter" /> public class CacheResourceFilter : IAsyncResourceFilter { private static readonly Dictionary<string, ObjectResult> _cache = new Dictionary<string, ObjectResult>(); /// <summary> /// Called asynchronously before the rest of the pipeline. /// </summary> /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ResourceExecutingContext" />.</param> /// <param name="next">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ResourceExecutionDelegate" />. Invoked to execute the next resource filter or the remainder /// of the pipeline.</param> public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { var cacheKey = context.HttpContext.Request.Path.ToString(); if (_cache != null && _cache.ContainsKey(cacheKey)) { var cacheValue = _cache[cacheKey]; if (cacheValue != null) { context.Result = cacheValue; } } else { var executedContext = await next(); var result = executedContext.Result as ObjectResult; if (result != null) { _cache.Add(cacheKey, result); } } } } ``` ### Action Filter 實作 `IAsyncActionFilter` 或 `IActionFilter` 這裡分別針對執行前和執行後做兩個範例 範例1 : 對執行前傳入的參數檢查,若檢查未通過則回傳參數驗證失敗 ```csharp= /// <summary> /// Class ValidationActionFilter /// </summary> /// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.IAsyncActionFilter" /> public class ValidationActionFilter : IAsyncActionFilter, IOrderedFilter { public int Order { get; set; } = 0; /// <summary> /// Called asynchronously before the action, after model binding is complete. /// </summary> /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext" />.</param> /// <param name="next">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutionDelegate" />. Invoked to execute the next action filter or the action itself.</param> public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var parameter = context.ActionArguments.SingleOrDefault(); if (parameter.Value is null) { var response = new FailResultViewModel { CorrelationId = Guid.NewGuid().ToString(), Method = $"{context.HttpContext.Request.Path}.{context.HttpContext.Request.Method}", Status = "Error", Version = "1.0", Error = new FailInformation { Domain = "ProjectName", Message = "參數驗證失敗", Description = "傳入參數為null" } }; context.Result = new ObjectResult(response) { // 400 StatusCode = (int)HttpStatusCode.BadRequest }; } else { await next(); } } } ``` 範例2 : 將執行後的結果再包裝成特定格式後回傳 ```csharp= /// <summary> /// Class MessageActionFilter /// </summary> /// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.IAsyncActionFilter" /> public class MessageActionFilter : IAsyncActionFilter, IOrderedFilter { public int Order { get; set; } = 0; /// <summary> /// Called asynchronously before the action, after model binding is complete. /// </summary> /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutingContext" />.</param> /// <param name="next">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ActionExecutionDelegate" />. Invoked to execute the next action filter or the action itself.</param> public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var executedContext = await next(); var result = executedContext.Result as ObjectResult; // ModelStateDictionary存放錯誤訊息 if (result != null && !(result.Value is HttpResponseMessage) && !(result.Value is SuccessResultViewModel<object>) && !(result.Value is FailResultViewModel) && !(result.Value is SuccessResultViewModel<ModelStateDictionary>)) { var responseModel = new SuccessResultViewModel<object> { Version = "1.0", Method = $"{context.HttpContext.Request.Path}.{context.HttpContext.Request.Method}", Status = "Success", CorrelationId = Guid.NewGuid().ToString(), Data = result.Value }; executedContext.Result = new ObjectResult(responseModel) { // 200 StatusCode = (int)HttpStatusCode.OK }; } } } ``` ActionExecutingContext 提供以下幾個參數 : * ActionArguments : 讀取輸入的值 * Controller : 管理 Controller * Result : 設定 `Result` 會影響後續的 Filter 運作 ActionExecutedContext 提供以下幾個參數 : * Canceled : 如果 Action 被其他 Filter 攔截並回傳則為`true` * Exception : 如果 Action 或是先前的 Action Filter 拋出 Exception 則不會是 null ### Exception Filter 實作 `IAsyncExceptionFilter` 或 `IExceptionFilter` 範例: 針對 Action 拋出的例外狀況進行攔截並包裝成特定格式後回傳 ```csharp= /// <summary> /// Class ExceptionFilter /// </summary> /// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.IAsyncExceptionFilter" /> public class ExceptionFilter : IAsyncExceptionFilter { /// <summary> /// Called after an action has thrown an <see cref="T:System.Exception" />. /// </summary> /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ExceptionContext" />.</param> /// <returns> /// A <see cref="T:System.Threading.Tasks.Task" /> that on completion indicates the filter has executed. /// </returns> public Task OnExceptionAsync(ExceptionContext context) { var response = new FailResultViewModel { CorrelationId = Guid.NewGuid().ToString(), Method = $"{context.HttpContext.Request.Path}.{context.HttpContext.Request.Method}", Status = "Error", Version = "1.0", Error = new FailInformation { Domain = "ProjectName", ErrorCode = 40000, Message = context.Exception.Message, Description = context.Exception.ToString() } }; context.Result = new ObjectResult(response) { // 500 StatusCode = (int)HttpStatusCode.InternalServerError }; // Exceptinon Filter只在ExceptionHandled=false時觸發 // 所以處理完Exception要標記true表示已處理 context.ExceptionHandled = true; return Task.CompletedTask; } } ``` ### Result Filter 實作 `IAsyncResultFilter` 或 `IResultFilter` 範例 : 將訊息加入 Header 回傳 ```csharp= /// <summary> /// Class ResultFilter /// </summary> /// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.IAsyncResultFilter" /> public class ResultFilter : IAsyncResultFilter { /// <summary> /// Called asynchronously before the action result. /// </summary> /// <param name="context">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ResultExecutingContext" />.</param> /// <param name="next">The <see cref="T:Microsoft.AspNetCore.Mvc.Filters.ResultExecutionDelegate" />. Invoked to execute the next result filter or the result itself.</param> public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) { if (!(context.Result is EmptyResult)) { var headerName = "OnResultExecuting"; var headerValue = new string[] { "ResultExecuting Successfully" }; context.HttpContext.Response.Headers.Add(headerName, headerValue); await next(); // 無法在執行後加入 Header,因為 Response 已經開始,此時 Response 可能已經到 Client 端那便無法修改了 } else { // 若已經被其他 Filter 攔截回傳或是接收到的 context 是空的,則取消 Result 回傳 // 但是若提前被攔截則並不會進到 ResultFilter context.Cancel = true; } } } ``` ## Filter 的執行順序 預設註冊相同類型的 Filter 是採用先進後出,依照註冊的順序和層級都會影響執行順序。 | 順序 | Filter 範圍 | Filter Method | | -------- | -------- | -------- | | 1 | Global | OnActionExecuting | | 2 | Controller或Razor Page | OnActionExecuting | | 3 | Method | OnActionExecuting | | 4 | Method | OnActionExecuted | | 5 | Controller或Razor Page | OnActionExecuted | | 6 | Global | OnActionExecuted | 可以透過實作 IOrderedFilter 來更改執行的順序 ```csharp= public class ValidationActionFilter : IAsyncActionFilter, IOrderedFilter { public int Order { get; set; } = 0; public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // ... } } ``` 在註冊 Filter 時,填入 Order 來決定執行順序,數字越小代表越先執行 ```csharp= public static class FilterExtensions { public static void AddMessageFilter(this MvcOptions options) { options.Filters.Add(new ValidationActionFilter() { Order = 0 }); options.Filters.Add(new MessageActionFilter() { Order = 1 }); } } ``` ## Filter 的使用方式 ### 註冊全域 Filter 建立一個擴充類別,填入要註冊的 Filter ```csharp= /// <summary> /// Class FilterExtensions /// </summary> public static class FilterExtensions { /// <summary> /// Adds the message filter. /// </summary> /// <param name="options">The options.</param> public static void AddMessageFilter(this MvcOptions options) { options.Filters.Add<AuthorizationFilter>(); options.Filters.Add<CacheResourceFilter>(); options.Filters.Add<ExceptionFilter>(); options.Filters.Add(new ValidationActionFilter() { Order = 0 }); options.Filters.Add(new MessageActionFilter() { Order = 1 }); options.Filters.Add<ResultFilter>(); } } ``` 前一步建立了擴充方法後便可以在 Startup 裡的 ConfigureServices 直接註冊使用 ```csharp= services.AddControllers(options => { options.AddMessageFilter(); }); ``` ### 使用 Attribute 套用特定 Controller 或 Action 在 Controller 或 Action 上加上 `[TypeFilter(type)]` 就可以進行區域註冊 ```csharp= [Route("api/[controller]")] [ApiController] [TypeFilter(typeof(ValidationActionFilter))] public class UserController : ControllerBase { [HttpGet("[action]")] [TypeFilter(typeof(ValidationActionFilter))] public async Task<UserViewModel> FindByUserIdAsync(int userId) { // ... } } ``` 使用 `[TypeFilter(type)]` 有點不直覺也太長了,可以直接在 Filter 繼承 Attribute ```csharp= public class ValidationActionFilter : IAsyncActionFilter, Attribute { public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // ... } } ``` `[Attribute]` 註冊就可以改成直接填入Filter名稱 ```csharp= [Route("api/[controller]")] [ApiController] [ValidationActionFilter] public class UserController : ControllerBase { [HttpGet("[action]")] [ValidationActionFilter] public async Task<UserViewModel> FindByUserIdAsync(int userId) { // ... } } ``` ## 參考 [1][Filters in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-3.1) [2][Filters 過濾器](https://ithelp.ithome.com.tw/articles/10208961) [3][ASP.NET Core 2 系列 - Filters](https://ithelp.ithome.com.tw/articles/10195407) [4][使用 Filter 統一 API 的回傳格式和例外處理](https://ithelp.ithome.com.tw/articles/10198206) [5][Exploring Middleware as MVC Filters in ASP.NET Core 1.1](https://andrewlock.net/exploring-middleware-as-mvc-filters-in-asp-net-core-1-1/) [6][Middleware vs Filters ASP. NET Core](https://dotnetcultist.com/middleware-vs-filters-asp-net-core/) [7][Filters in ASP.NET Core MVC](https://code-maze.com/filters-in-asp-net-core-mvc/) ###### tags: `C#` `Filter`