ASP.NET WebApi 2 提供了三個好用的 FilterAttribute 讓開發者可以擴充後將這些 FilterAttribute 套用在 Controller / Action 上面, 也可以在 WebApiConfig.cs 或 Global.asax 裡面註冊 , 並在一個 API request 生命週期中的特定時間被執行.
這樣做的好處是可以將跟 API 主業務邏輯無關, 卻又是大部分 API 都要做的事收斂起來, 只需要將 Attribute 套用在正確的地方就能一體適用, 開發者就能更專注在 API 的主要商務邏輯層面.
在了解 FilterAttribute 的運用之前, 必須先知道 Attribute 是什麼以及如何使用 Attribute.
三種 FilterAttribute 介紹
ASP.NET WebApi 2 提供了三個好用的 FilterAttribute, 分別是 AuthorizationFilterAttribute, ActionFilterAttribute 與 ExceptionFilterAttribute , 要注意的是命名空間都是 System.Web.Http.Filters, ASP.NET MVC 也有提供同名的 FilterAttribute, 但是命名空間不同, 內容也不盡相同.
這三個 FilterAttribute 提供的功能各異:
AuthorizationFilterAttribute 提供的是驗證相關的流程, 能在進 Action 之前就先執行相關的驗證方法.
ActionFilterAttribute 會分別在 Action 執行前與執行完畢後執行相關方法.
ExceptionFilterAttribute 在例外(unhandled exception)發生後執行.
基本用法
這三種 FilterAttribute 的使用方式都一樣, 這邊用 ExceptionFilterAttribute 來當作範例.
- 繼承對應的 FilterAttribute, 並覆寫父類別中的相關方法.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public class DemoExceptionFilterAttribute : ExceptionFilterAttribute { public override void OnException(HttpActionExecutedContext actionExecutedContext) { base.OnException(actionExecutedContext);
actionExecutedContext.Response = new HttpResponseMessage(HttpStatusCode.InternalServerError); } }
|
套用 FilterAttribute.
1 2 3 4 5 6 7 8
| public class DemoController : ApiController { [DemoExceptionFilter] public IEnumerable<string> Get() { throw new Exception(); } }
|
1 2 3 4 5 6 7 8
| [DemoExceptionFilter] public class DemoController : ApiController { public IEnumerable<string> Get() { throw new Exception(); } }
|
更進一步, 可以將這些 FilterAttribute 套用在一個共用的 BaseApiController 上, 只要繼承 BaseApiController, 就能直接套用.
1 2 3 4 5 6 7 8 9 10 11 12 13
| [DemoExceptionFilter] public class BaseApiController : ApiController {
}
public class DemoController : BaseApiController { public IEnumerable<string> Get() { throw new Exception(); } }
|
- 套用到所有 ApiController - 從
~/App_Start/WebApiConfig.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class DemoController : ApiController { public IEnumerable<string> Get() { throw new Exception(); } }
public static void Register(HttpConfiguration config) {
config.Filters.Add(new DemoExceptionFilterAttribute());
}
|
- 套用到所有 ApiController - 從
~/Global.asax
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class DemoController : ApiController { public IEnumerable<string> Get() { throw new Exception(); } }
protected void Application_Start() {
GlobalConfiguration.Configuration.Filters.Add(new DemoExceptionFilterAttribute());
}
|
- 單獨套用只能套用在 ApiController 的衍生類別或 Action 上, 如果是套用在其他方法(例如: xxxService.Do(), 或 this.Do()), 即使那個方法有被 ApiController 的 Action 呼叫到也是無法讓 FilterAttribute 運作的.
- 重複套用同一個 FilterAttribute 不會壞, 但因為註冊了兩次, 所以也會執行兩次, 所以應該要特別注意套用方式避免重複套用.
排除特定 Action 或 Controller
如果大部分 API 都需要套用一個 FilterAttribute, 但有少數不套用, 應該要怎麼實作排除機制呢?
- 實作標記為排除的 Attribute
1 2 3 4 5 6 7 8 9
| public class IgnoreFilterAttribute : Attribute { public Type FilterType { get; }
public IgnoreFilterAttribute(Type filterType) { this.FilterType = filterType; } }
|
- 擴充 FilterAttribute 以支援排除功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public class DemoExceptionFilterAttribute : ExceptionFilterAttribute { public override void OnException(HttpActionExecutedContext actionExecutedContext) { Func<IgnoreFilterAttribute, bool> ignoreCheck = (r) => { return r.FilterType.IsAssignableFrom(typeof(DemoExceptionFilterAttribute)); };
var ignoredActions = actionExecutedContext .ActionContext .ActionDescriptor .GetCustomAttributes<IgnoreFilterAttribute>() .Any(ignoreCheck);
var ignoredControllers = actionExecutedContext .ActionContext .ControllerContext .ControllerDescriptor .GetCustomAttributes<IgnoreFilterAttribute>() .Any(ignoreCheck);
if (ignoredActions || ignoredControllers) { return; }
} }
|
- 套用標記為排除的 Attribute
1 2 3 4 5 6 7 8 9 10 11
|
public class DemoController : ApiController { [IgnoreFilter(typeof(DemoExceptionFilterAttribute))] public IEnumerable<string> Get() { throw new Exception(); } }
|
執行順序
根據 The ASP.NET Web API 2 HTTP Message Lifecycle in 43 Easy Steps 的說明加上 ASP.NET WEB API 2: HTTP MESSAGE LIFECYLE
流程圖的內容來看, AuthorizationFilterAttribute 會先執行, 然後才是 ActionFilterAttribute, 而例外發生時會執行ExceptionFilterAttribute, 至於同種類的多個 FilterAttribute 或是一個 FilterAttribute 中的不同方法執行的順序, 可以從下面的程式碼觀察出結果.
首先, 需要 AuthorizationFilterAttribute 和 ActionFilterAttribute 各兩組, 另外一組 ExceptionFilterAttribute, 接著把這些 FilterAttribute 放在 WebApi 專案預設的 Action 上, 且 ExceptionFilterAttribute 刻意插在中間, 而 Action 就簡單的拋出一個例外.
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class ValuesController : ApiController { [MyAuth2] [MyAuth1] [MyAction1] [MyException1] [MyAction2] public IEnumerable<string> Get() { throw new Exception(); } }
|
實作部分所有 FilterAttribute 並覆寫所有能覆寫的方法, 內容只是單純寫個簡單的歷程記錄到 RouteTracer.Routes 中, 但ExceptionFilterAttribute 需要另外負責回傳正確的狀態與 RouteTracer.Routes 內容, 才能順利觀察執行順序.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
| public class MyAuth1 : AuthorizationFilterAttribute { public override void OnAuthorization(HttpActionContext actionContext) { RouteTracer.Routes.Add("MyAuth1.OnAuthorization"); base.OnAuthorization(actionContext); }
public override Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken cancellationToken) { RouteTracer.Routes.Add("MyAuth1.OnAuthorizationAsync"); return base.OnAuthorizationAsync(actionContext, cancellationToken); } }
public class MyAuth2 : AuthorizationFilterAttribute { public override void OnAuthorization(HttpActionContext actionContext) { RouteTracer.Routes.Add("MyAuth2.OnAuthorization"); base.OnAuthorization(actionContext); }
public override Task OnAuthorizationAsync(HttpActionContext actionContext, CancellationToken cancellationToken) { RouteTracer.Routes.Add("MyAuth2.OnAuthorizationAsync"); return base.OnAuthorizationAsync(actionContext, cancellationToken); } }
public class MyAction1 : ActionFilterAttribute { public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) { RouteTracer.Routes.Add("MyAction1.OnActionExecuted"); base.OnActionExecuted(actionExecutedContext); }
public override Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) { RouteTracer.Routes.Add("MyAction1.OnActionExecutedAsync"); return base.OnActionExecutedAsync(actionExecutedContext, cancellationToken); }
public override void OnActionExecuting(HttpActionContext actionContext) { RouteTracer.Routes.Add("MyAction1.OnActionExecuting"); base.OnActionExecuting(actionContext); }
public override Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken) { RouteTracer.Routes.Add("MyAction1.OnActionExecutingAsync"); return base.OnActionExecutingAsync(actionContext, cancellationToken); } }
public class MyAction2 : ActionFilterAttribute { public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) { RouteTracer.Routes.Add("MyAction2.OnActionExecuted"); base.OnActionExecuted(actionExecutedContext); }
public override Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) { RouteTracer.Routes.Add("MyAction2.OnActionExecutedAsync"); return base.OnActionExecutedAsync(actionExecutedContext, cancellationToken); }
public override void OnActionExecuting(HttpActionContext actionContext) { RouteTracer.Routes.Add("MyAction2.OnActionExecuting"); base.OnActionExecuting(actionContext); }
public override Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken) { RouteTracer.Routes.Add("MyAction2.OnActionExecutingAsync"); return base.OnActionExecutingAsync(actionContext, cancellationToken); } }
public class MyException1 : ExceptionFilterAttribute { public override void OnException(HttpActionExecutedContext actionExecutedContext) { RouteTracer.Routes.Add("MyException1.OnException"); base.OnException(actionExecutedContext); actionExecutedContext.Response = new HttpResponseMessage(HttpStatusCode.OK); actionExecutedContext.Response.Content = new ObjectContent<List<string>>(RouteTracer.Routes, new XmlMediaTypeFormatter()); } }
|
實際運作後頁面顯示結果如下, 可以看出各種 FilterAttribute 以及其中的方法的執行順序.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <ArrayOfstring xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/2003/10/Serialization/Arrays"> <string>MyAuth2.OnAuthorizationAsync</string> <string>MyAuth2.OnAuthorization</string> <string>MyAuth1.OnAuthorizationAsync</string> <string>MyAuth1.OnAuthorization</string> <string>MyAction1.OnActionExecutingAsync</string> <string>MyAction1.OnActionExecuting</string> <string>MyAction2.OnActionExecutingAsync</string> <string>MyAction2.OnActionExecuting</string> <string>MyAction2.OnActionExecutedAsync</string> <string>MyAction2.OnActionExecuted</string> <string>MyAction1.OnActionExecutedAsync</string> <string>MyAction1.OnActionExecuted</string> <string>MyException1.OnException</string> </ArrayOfstring>
|
就執行順序的這部分, 總結來說
AuthorizationFilterAttribute 先於 ActionFilterAttribute 執行 (從流程圖上來看合理).
- 多個同種類的 FilterAttribute 實作, 會依照套用的順序執行, 但
OnActionExecuted 與 OnActionExecutedAsync 是反序 (從流程圖上來看也合理, 因為 OnActionExecuted 系列方法是在 Action 執行後要 response 時才執行).
- 如果有例外發生的話,
ExceptionFilterAttribute 會在最後才執行.
- 一個 FilterAttribute 內部的方法執行順序看起來是非同步方法先於同步方法.
- 單一測試就下結論其實不穩妥, 所以我後來又做了幾次不一樣的排序實驗, 看起來結果是符合推測的, 但無法完全保證.
- 有些特性可能會隨著版本的變遷改變, 如果實際專案要用, 又真的很在意這些順序或其他細節的話, 還是要再就實際狀況測試一輪.
結論
想像一下, 當有一天團隊要導入 log 收集與分析工具, 需要統一 log 的格式時, 如果當初有用 FilterAtrribute 來印通用訊息就可以省下非常多瑣碎的工, 而例外處理也是同理, 且能避免到處都是為了抓 unhandled exception 而寫的 try-catch 語句.
參考
How to disable a global filter in ASP.Net MVC selectively
ASP.NET Web API Exception Filter
Exclude A Filter
The ASP.NET Web API 2 HTTP Message Lifecycle in 43 Easy Steps
ASP.NET WEB API 2: HTTP MESSAGE LIFECYLE