(寫完本文後,我去下載了ASP.NET MVC 3 RC,發現它對Filter的可控性方面進行了某些增強——不僅僅是針對全域Filter的GlobalFilterCollection——所以在此特別說明一下本文目前主要針對的是ASP.NET MVC 2.0 RTM,當然大部分都適用於3.0)
以AuthorizeAttribute這個Filter舉例,一個Controller有若干個Action,包括登入的Action(如Login)。這時我們有兩種方式來實現:
1、重新實現一個IAuthorizationFilter,在裡面判斷如果是Login這個Action,就不進行驗證。然後將這個Filter作為FilterAttribute置於Controller定義上。或者Controller自身實現IAuthorizationFilter。
2、除了Login這個Action之外的所有Action加上個Authorize。
這兩種方式雖然能夠達到目的,但總覺得不夠優雅。
如果我想給所有Action注入一段html到頁面底部,這種注入可能是臨時的,我必須去修改Controller嗎?(ASP.NET MVC 3 可以將Filter加入GlobalFilters集合中)
如果我想動態控制某個Action允許由哪些角色訪問,我通過修改Controller能實現嗎?
如果我想這時候控制的由哪些角色來訪問,需求改變時我想要控制由哪些使用者來訪問呢?我還得去修改Controller嗎?或者增加或修改Filter嗎?
在ASP.NET MVC 2 中,以上需求好像都需要重新編譯Controller。
這裡整理一下我們的需求:能不能增刪改Filter時不去修改Controller?能讓所有Controller和Action定義處都乾乾淨淨的那就最好了。
那就把Filter放在Controller外部來管理,在Action或ActionResult等執行Filter之前保證將需要的Filter準備好就行了。
在解決問題之前,先簡單回顧一下Action執行前後發生的事。
我們知道,在ASP.NET MVC中,每一次請求通常都定位到一個具體的Controller的Action中。
在預設情況下,由Action執行器(ControllerActionInvoker去控制Action的執行(或不執行),實際做事的是InvokeAction方法。
InvokeAction方法首先去尋找Action(由FindAction方法),如果Action被找到了,會通過反射的方式去檢索該Action所屬Controller的擁有的Filter以及Action擁有的Filter,如果Controller本身也實現了某些Filter介面,也會被檢索到(由GetFilters方法)。將找到的所有Filter放入一個FilterInfo變數中(當然放入FilterInfo變數的Filter是經過排序和重複清理的),FilterInfo中儲存的Filter不完全是Action自己的Attribute上定義的。
然後先執行找到的所有IAuthorizationFilter(由InvokeAuthorizationFilters方法)。在InvokeAuthorizationFilters方法中,只要有一個IAuthorizationFilter的ActionResult不為null就會立即返回到調用處,不會執行其他的IAuthorizationFilter了。InvokeAuthorizationFilters執行完成後,InvokeAction方法檢測其執行結果,如果ActionResult不為null,則執行該Result,其他的IActionFilter,IActionResult就不管啦,否則InvokeAction方法繼續。
接著擷取要傳給Action的參數集(交給GetParameterValue方法),就執行InvokeActionMethodWithFilters方法,方法名已經足夠說明它是幹什麼的了。
如果一切正常,根據InvokeActionMethodWithFilters方法返回的結果去接著就執行InvokeActionResultWithFilters方法。
在執行InvokeAuthorizationFilters一直到執行InvokeActionResultWithFilters的這一整個過程中如果發生異常,則根據捕獲的異常執行InvokeExceptionFilters進行異常處理。
代碼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;
}
這裡需要注意一點:InvokeAction、GetFilters、InvokeAuthorizationFilters、GetParameterValue、InvokeActionMethodWithFilters、InvokeActionResultWithFilters、InvokeExceptionFilters等全是虛方法,除非有足夠的原因去繼承IActionInvoker重寫一個Action執行器,否則我覺得重寫某些方法足夠滿足我們的擴充需求了。
甚至ControllerActionInvoker類本身,在ASP.NET MVC基礎架構中也是可以替換的,怎麼替換呢?在繼承Controller類實現我們自己的Controller時重寫CreateActionInvoker方法就可以。
另外還可以在構造Controller對象給它的ActionInvoker屬性賦值,這又怎麼賦值?重寫DefaultControllerFactory建立Controller執行個體的GetControllerInstance方法。 然後在Applicaion_Start中設定新的ControllerFactor:
ControllerBuilder.Current.SetControllerFactory(new YourControllerFactory());
回到主題。 我們將Filter和Action的對應關係或Filter和Controller的對應關係存於一個集合中並緩衝起來。在合適的位置“注入”進去就行了。
從找到Action到執行InvokeAuthorizationFilters之前,必須將IAuthorizationFilter準備好;從找到Action到執行InvokeActionMethodWithFilters內部執行Action之前,必須將IActionFilter準備好;從找到Action到執行InvokeActionResultWithFilters內部執行ActionResult之前,必須將IResultFilter準備好。異常發生InvokeExceptionFilters執行之前,必須將IExceptionFilter準備好。 基於以上幾點,我們好像可以在FindAction方法GetFilters方法、InvokeAuthorizationFilters方法、InvokeActionResultWithFilters方法和GetFilters方法內部,或InvokeExceptionFilters執行前將必要的Filter準備好就可以了。當然,最合適的莫過於GetFilters方法了。
在GetFilters方法中,我們可以根據當前Action的特徵(如方法名,或包括請求方式Get或Post等),在“Filter和Action對應表”進行檢索;或者根據當前Controller的特徵(類型、完整類名都可以),在"Filter和Controller對應表"中進行檢索。將匹配的Filter累加入FilterInfo變數就可以了。
感興趣的可以去看看Oxite ,它實現了本章提到的一部分的需求。