一個Action Filter是一個特性(Attribute),定義了橫切在Action方法執行前后的行為。一個Action Filter可以應用在某個Action方法上,也可以應用在整個Controller上。
所有Action Filter必須繼承自FilterAttribute,同時必須實現至少一個以下接口:
IAuthorizationFilter,負責認證和授權,包含一個OnAuthorization方法。
IActionFilter,在Action方法執行前后調用,包含OnActionExecuting和OnActionExecuted兩個方法。
IResultFilter,在ActionResult執行前后調用,包含OnResultExecuting和OnResultExecuted兩個方法。
IExceptionFilter,當發生異常時調用,不管是在Action方法中、Result執行中或是前面三種Filter的執行過程中產生的未處理異常都會觸發IExceptionFilter的OnException方法。
以上幾種ActionFilter的執行順序就是按照上面的順序,相同種類的ActionFiler按順序執行。接口中的配對方法需要配對執行。
ASP.Net MVC內建了幾個ActionFilter供我們使用:
ActionFilterAttribute,實現了IActionFilter和IResultFilter,但是方法內部什么也不做,供繼承它的子類重寫。
OutputCacheAttribute,緩存Action的執行輸出結果,它繼承了ActionFilterAttribute,又實現了IExceptionFilter接口。
HandleErrorAttribute,實現了IExceptionFilter,供開發人員選擇一個View來處理Exception
AuthorizeAttribute,實現了IAuthorizationFilter,提供認證和授權的功能,開發人員可以選擇繼承它然后覆蓋它的方法來實現自己的認證和授權機制。
下面我打算結合源碼深入理解一下這些Filters。
ControllerActionInvoker類負責Action方法的調用,它的InvokeAction方法顯示了Action方法調用,ActionResult執行和Filter之間的關系。
public virtual bool InvokeAction(ControllerContext controllerContext, string actionName) {
if (controllerContext == null) {
throw new ArgumentNullException("controllerContext");
}
if (String.IsNullOrEmpty(actionName)) {
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "actionName");
}
ControllerDescriptor controllerDescriptor = GetControllerDescriptor(controllerContext);
ActionDescriptor actionDescriptor = FindAction(controllerContext, controllerDescriptor, actionName);
if (actionDescriptor != null) {
FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor);
try {
AuthorizationContext authContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor);
if (authContext.Result != null) {
// the auth filter signaled that we should let it short-circuit the request
InvokeActionResult(controllerContext, authContext.Result);
}
else {
if (controllerContext.Controller.ValidateRequest) {
ValidateRequest(controllerContext);
}
IDictionary<string, object> parameters = GetParameterValues(controllerContext, actionDescriptor);
ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters);
InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, postActionContext.Result);
}
}
catch (ThreadAbortException) {
// This type of exception occurs as a result of Response.Redirect(), but we special-case so that
// the filters don't see this as an error.
throw;
}
catch (Exception ex) {
// something blew up, so execute the exception filters
ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex);
if (!exceptionContext.ExceptionHandled) {
throw;
}
InvokeActionResult(controllerContext, exceptionContext.Result);
}
return true;
}
// notify controller that no method matched
return false;
}
這段代碼先獲取了Controller、Action、Filter的描述信息
ControllerDescriptor controllerDescriptor = GetControllerDescriptor(controllerContext);
ActionDescriptor actionDescriptor = FindAction(controllerContext, controllerDescriptor, actionName);
FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor);
接着使用這些描述信息,在恰當的時候先后調用了AuthorizationFilters、ActionFilters、ResultFilters、ExceptionFilters,幾個關鍵方法是:
AuthorizationContext authContext = InvokeAuthorizationFilters(controllerContext, filterInfo.AuthorizationFilters, actionDescriptor);//調用AuthorizationFilters
ActionExecutedContext postActionContext = InvokeActionMethodWithFilters(controllerContext, filterInfo.ActionFilters, actionDescriptor, parameters);//執行Action方法,同時調用ActionFilters
InvokeActionResultWithFilters(controllerContext, filterInfo.ResultFilters, postActionContext.Result); //執行ActionResult,同時調用ResultFilters
ExceptionContext exceptionContext = InvokeExceptionFilters(controllerContext, filterInfo.ExceptionFilters, ex);//如果上面的過程中拋出未處理的異常,調用ExceptionFilter
閱讀這些代碼,就可以詳細了解Action和Filter的執行過程,我總結幾點。
1. 調用AuthorizationFilters
ControllerActionInvoker.InvokeAuthorizationFilters方法依次調用每個注冊的AuthorizationFilter的OnAuthorization方法,一旦發現在某個OnAuthorization方法調用過程中設置了ActionResult,就會停止繼續執行,並且跳過Action方法,直接執行這個ActionResult,返回結果給客戶端。如果在OnAuthorization方法中拋出未捕獲異常,會直接跳至ExceptionFilters。
2. 執行Action方法,同時調用ActionFilters
ControllerActionInvoker.InvokeActionMethodWithFilters方法負責這個過程。
我們先考慮只有一個ActionFilter的情況。
首先執行ActionFilter的OnActionExecuting方法,如果在方法中設置了傳入的上下文的Result屬性,執行將被短路,直接返回這個Result,Action方法和ActionFilter的OnActionExecuted方法都不會被執行。
如果ActionFilter的OnActionExecuting方法拋出異常,將直接跳轉至ExceptionFilters。
如果ActionFilter的OnActionExecuting方法正常返回且未設置Result屬性,接下來執行Action方法,如果正常返回一個ActionResult,就將這個Result傳遞給ActionFilter的OnActionExecuted方法(作為上下文參數的Result屬性)。如果Action方法中拋出異常,仍然會調用ActionFilter的OnActionExecuted方法,這時傳給該方法的上下文對象的Result屬性為null,Exception屬性為Action方法拋出的異常對象,如果這時在OnActionExecuted方法中設置了上下文的ExceptionHandled為true,該異常不會再拋出,ExceptionHandled針對的是Action方法拋出的異常,如果是在OnActionExecuted方法的執行內部拋出異常,不管有沒有設置ExceptionHandled為true都會拋出該異常,流程跳轉至ExceptionFilters。
現在我們考慮有多個ActionFilter的情況,假設有FilterA和FilterB,調用流程如下圖
執行FilterB的情況與只有一個ActionFilter的情形一樣,前面已經討論過了。在執行FilterA的方法時,FilterA將FilterB和Action方法看成一個整體,如果這個整體拋出異常,FilterA.OnActionExecuted方法的上下文中的Exception屬性會被設為這個異常供我們處理,就像FilterB.OnActionExecuted方法的上下文中可能包含Action方法執行時拋出的異常一樣。
由此看出FilterA的執行和FilterB的執行的內部邏輯是一樣的,就像同一個函數被嵌套着調用,ASP.Net MVC 源代碼巧妙的使用了linq中的聚合方法Aggregate進行多個Filter的迭代,使用多層嵌套的lambda表達式來表示迭代的結果,迭代的結果是一個lambda表達式,即一個函數入口指針,指向最外面的Filter的執行起點。
protected virtual ActionExecutedContext InvokeActionMethodWithFilters(ControllerContext controllerContext, IList<IActionFilter> filters, ActionDescriptor actionDescriptor, IDictionary<string, object> parameters) {
ActionExecutingContext preContext = new ActionExecutingContext(controllerContext, actionDescriptor, parameters);
Func<ActionExecutedContext> continuation = () =>
new ActionExecutedContext(controllerContext, actionDescriptor, false /* canceled */, null /* exception */) {
Result = InvokeActionMethod(controllerContext, actionDescriptor, parameters)
};
// need to reverse the filter list because the continuations are built up backward
Func<ActionExecutedContext> thunk = filters.Reverse().Aggregate(continuation,
(next, filter) => () => InvokeActionMethodFilter(filter, preContext, next));
return thunk();
}
3. 執行ActionResult,同時調用ResultFilters
ControllerActionInvoker.InvokeActionResultWithFilters方法負責這個過程,考慮只有一個ResultFilter的情況,首先執行Filter的OnResultExecuting方法,在這個方法中可以重置ActionResult。如果在這個方法內將上下文的Cancel屬性設為true,將停止ActionResult的執行,也就是說不會再執行ActionResult. ExecuteResult和Filter的OnResultExecuted方法,瀏覽器得不到返回。如果在OnResultExecuting方法內拋出異常,流程會跳轉至ExceptionFilters。
接着執行ActionResult,即調用該ActionRsult的ExecuteResult方法,向瀏覽器輸出結果,如果這時拋出異常,會將這個異常先報告給Filter的OnResultExecuted方法,OnResultExecuted方法內部可以檢查上下文的Exception屬性,如果不為null,可以處理這個異常,如果不需要繼續拋出,可以設置上下文的ExceptionHandled屬性為true。
最后執行Filter的OnResultExecuted方法。
多個ResultFilter的情況與上面討論多個ActionFilter的情況類似,就不贅述了。
4. 如果上面的過程中拋出未處理的異常,調用ExceptionFilter
ControllerActionInvoker.InvokeExceptionFilters方法負責這個過程。前面的Filters或Action方法執行拋出的異常都會被ExceptionFilter捕獲到,可以在這里統一處理異常。InvokeExceptionFilters方法會依次調用每一個ExceptionFilter的OnException方法,如果在OnException方法中設置了上下文的Result屬性並且將上下文的ExceptionHandled屬性設為true,就會執行這個ActionResult輸出內容給瀏覽器,一般會返回一個友好的錯誤頁面,如果不設置ExceptionHandled為true,會繼續拋出異常,最終用戶看到異常信息的黃頁。
ASP.Net MVC 自帶了一個好用的ExceptionFilter:HandleErrorAttribute,可以應用在Action方法上,它包含三個屬性:ExceptionType(處理的異常類型,默認為typeof(Exception)),Master(母版頁名稱),View(視圖名稱,默認為“Error”),HandleError的OnException方法會給上下文返回一個指向屬性中設置的View和Master名稱的ViewResult,同時將ControllerName,ActionName還有Exception對象作為視圖的Model對象的屬性傳給這個視圖,視圖的Model對象的類型為HandleErrorInfo類型,它包括ControllerName、ActionName、Exception三個屬性。
ASP.Net MVC 還提供了兩個重要的Filter實現:
AuthorizeAttribute:用於用戶認證和授權
OutputCacheAttribute:用於緩存Action執行結果
我會在下一篇文章中詳細介紹它們。