ASP.NET MVC:Filter和Action的執行介紹_實用技巧

來源:互聯網
上載者:User

根據controller的名字正確的執行個體化了一個controller對象。回到MVCHandler的BeginProcessRequest方法,可以看到,當得到controller對象之後,首先判斷它是不是IAsyncController,如果是則會建立委託用來非同步執行。通常情況下,我們都是繼承自Controller類,這不是一個IAsyncController,於是會直接執行Controller的Execute方法。Execute方法是在Controller的基類ControllerBase中定義的,這個方法除去一些安全檢查,初始化了ControllerContext(包含了ControllerBase和Request的資訊),核心是調用了ExecuteCore方法,這在ControllerBase是個抽象方法,在Controller類中有實現:

複製代碼 代碼如下:

protected override void ExecuteCore() {
PossiblyLoadTempData();
try {
string actionName = RouteData.GetRequiredString("action");
if (!ActionInvoker.InvokeAction(ControllerContext, actionName)) {
HandleUnknownAction(actionName);
}
}
finally {
PossiblySaveTempData();
}}

這個方法比較簡單,首先是載入臨時資料,這僅在是child action的時候會出現,暫不討論。接下來就是擷取action的名字,然後InvokeAction, 這裡的ActionInvoker是一個ControllerActionInvoker類型的對象,我們來看它的InvokeAction方法,
複製代碼 代碼如下:

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;}

這是一個非常核心的方法,有很多工作在這裡面完成。ASP.NET MVC中有幾個以Descriptor結尾的類型,首先獲得ControllerDescriptor,這個比較簡單,實際返回的是ReflectedControllerDescriptor對象。第二步實際上是調用了ReflectedControllerDescriptor的FindAction方法,獲得ActionDescriptor,ActionDescriptor最重要的屬性是一個MethodInfo,這就是當前action name對應的Action的方法。FindAction方法內部實際上是調用了ActionMethodSelector的FindActionMethod來獲得MethodInfo,可以想象,這個方法將會反射controller的所有方法的名字,然後和action name匹配,實際上,ASP.NET還支援一些額外的功能,主要是: 1.通過ActionNameAttribute屬性重新命名action的名字;2.支援ActionMethodSelectorAttribute對action方法進行篩選,比如[HttpPost]之類的。下面簡單看下ActionMethodSelector的實現,大致分為4步,首先是在建構函式中調用了如下方法反射controller中的所有action方法:
複製代碼 代碼如下:

private void PopulateLookupTables() {
MethodInfo[] allMethods = ControllerType.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Instance | BindingFlags.Public);
MethodInfo[] actionMethods = Array.FindAll(allMethods, IsValidActionMethod);
AliasedMethods = Array.FindAll(actionMethods, IsMethodDecoratedWithAliasingAttribute);
NonAliasedMethods = actionMethods.Except(AliasedMethods).ToLookup(method => method.Name, StringComparer.OrdinalIgnoreCase);
}FindActionMethod方法如下:
public MethodInfo FindActionMethod(ControllerContext controllerContext, string actionName) {
List<MethodInfo> methodsMatchingName = GetMatchingAliasedMethods(controllerContext, actionName);
methodsMatchingName.AddRange(NonAliasedMethods[actionName]);
List<MethodInfo> finalMethods = RunSelectionFilters(controllerContext, methodsMatchingName);
switch (finalMethods.Count) {
case 0:
return null;
case 1:
return finalMethods[0];
default:
throw CreateAmbiguousMatchException(finalMethods, actionName);
} }

這個方法是很清晰的,找到重新命名之後符合的,本身名字元合的,然後所有的方法判斷是否滿足ActionMethodSelectorAttribute的條件,最後或者返回匹配的MethodInfo,或者拋出異常,或者返回null。三個步驟的實現並不困難,不再分析下去。
第三步是得到Filter。 FilterInfo filterInfo = GetFilters(controllerContext, actionDescriptor);實際調用的是:
FilterProviders.Providers.GetFilters(controllerContext, actionDescriptor);這裡的代碼風格和之前的不太一樣,特別喜歡用各種委託,讀代碼有點困難,估計不是同一個人寫的。下面的分析都直接給出實際執行的代碼。首先看下FilterProvider的建構函式:
複製代碼 代碼如下:

static FilterProviders() {
Providers = new FilterProviderCollection();
Providers.Add(GlobalFilters.Filters);
Providers.Add(new FilterAttributeFilterProvider());
Providers.Add(new ControllerInstanceFilterProvider());
}

回憶下ASP.NET給Action加上filter的方法一共有如下幾種:
1. 在Application_Start註冊全域filter
2. 通過屬性給Action方法或者Controller加上filter
3. Controller類本身也實現了IActionFilter等幾個介面。通過重寫Controller類幾個相關方法加上filter。
這三種方式就對應了三個FilterProvider,這三個Provider的實現都不是很困難,不分析了。到此為止,準備工作都好了,接下來就會執行Filter和Action,ASP.NET的Filter一共有4類:


Filter Type Interface Description
Authorization IAuthorizationFilter Runs first
Action IActionFilter Runs before and after the action method
Result IResultFilter Runs before and after the result is executed
Exception IExceptionFilter Runs if another filter or action method throws an exception
下面看其原始碼的實現,首先就是InvokeAuthorizationFilters:
複製代碼 代碼如下:

protected virtual AuthorizationContext InvokeAuthorizationFilters(ControllerContext controllerContext, IList<IAuthorizationFilter> filters, ActionDescriptor actionDescriptor) {
AuthorizationContext context = new AuthorizationContext(controllerContext, actionDescriptor);
foreach (IAuthorizationFilter filter in filters) {
filter.OnAuthorization(context);
if (context.Result != null) {
break;
}
}
return context;}

注意到在實現IAuthorizationFilter介面的時候,要表示驗證失敗,需要在OnAuthorization方法中將參數context的Result設定為ActionResult,表示驗證失敗後需要顯示的頁面。接下來如果驗證失敗就會執行context的Result,如果成功就要執行GetParameterValues獲得Action的參數,在這個方法內部會進行Model Binding,這也是ASP.NET的一個重要特性,另文介紹。再接下來會分別執行InvokeActionMethodWithFilters和InvokeActionResultWithFilters,這兩個方法的結構是類似的,只是一個是執行Action方法和IActionFilter,一個是執行ActionResult和IResultFilter。以InvokeActionMethodWithFilters為例分析下:
複製代碼 代碼如下:

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();
}

這段代碼有點函數式的風格,不熟悉這種風格的人看起來有點難以理解。 用函數式程式設計語言的話來說,這裡的Aggregate其實就是foldr,
foldr::(a->b->b)->b->[a]->b
foldr 接受一個函數作為第一個參數,這個函數的參數有兩個,類型為a,b,傳回型別為b,第二個參數是類型b,作為起始值,第三個參數是一個類型為a的數組,foldr的功能是依次將數組中的a 和上次調用第一個參數函數(f )的傳回值作為f的兩個參數進行調用,第一次調用f的時候用起始值。對於C#來說,用物件導向的方式表示,是作為IEnummerable的一個擴充方法實現的,由於C# 不能直接將函數作為函數的參數傳入,所以傳入的是委託。說起來比較拗口,看一個例子:
複製代碼 代碼如下:

static void AggTest()
{
int[] data = { 1, 2, 3, 4 };
var res = data.Aggregate("String", (str, val) => str + val.ToString());
Console.WriteLine(res);
}

最後輸出的結果是String1234. 回到InvokeActionMethodWithFilters的實現上來,這裡對應的類型a是IActionFilter,類型b是Func<ActionExecutedContext>,初始值是continuation。假設我們有3個filter,[f1,f2,f3],我們來看下thunk最終是什麼,
第一次: next=continue, filter=f1, 傳回值 ()=>InvokeActionMethodFilter(f1, preContext, continue)
第二次:next=()=>InvokeActionMethodFilter(f1, preContext, continue), filter=f2
傳回值:()=>InvokeActionMethodFilter(f2, preContext,()=> InvokeActionMethodFilter(f1, preContext, continue)),
最終: thunk= ()=>InvokeActionMethodFilter(f3,preContext,()=>InvokeActionMethodFilter(f2, preContext, ()=>InvokeActionMethodFilter(f1, preContext, continue)));
直到 return thunk()之前,所有真正的代碼都沒有執行,關鍵是構建好了thunk這個委託,把thunk展開成上面的樣子,應該比較清楚真正的調用順序什麼樣的了。這裡花了比較多的筆墨介紹了如何通過Aggregate方法構造調用鏈,這裡有一篇文章專門介紹了這個,也可以參考下。想象下,如果filter的功能就是先遍曆調用f的Executing方法,然後調用Action方法,最後再依次調用f的Executed方法,那麼完全可以用迭代來實現,大可不必如此抽象複雜,關鍵是ASP.NET MVC對於filter中異常的處理還有一些特殊之處,看下InvokeActionMethodFilter的實現:
複製代碼 代碼如下:

internal static ActionExecutedContext InvokeActionMethodFilter(IActionFilter filter, ActionExecutingContext preContext, Func<ActionExecutedContext> continuation) {
filter.OnActionExecuting(preContext);
if (preContext.Result != null) {
return new ActionExecutedContext(preContext, preContext.ActionDescriptor, true /* canceled */, null /* exception */) {
Result = preContext.Result
};
}
bool wasError = false;
ActionExecutedContext postContext = null;
try {
postContext = continuation();
}
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.
postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, null /* exception */);
filter.OnActionExecuted(postContext);
throw;
}
catch (Exception ex) {
wasError = true;
postContext = new ActionExecutedContext(preContext, preContext.ActionDescriptor, false /* canceled */, ex);
filter.OnActionExecuted(postContext);
if (!postContext.ExceptionHandled) {
throw;
}
}
if (!wasError) {
filter.OnActionExecuted(postContext);
}
return postContext;
}

代碼有點長,首先就是觸發了filter的OnActionExecuting方法,這是方法的核心。接下來的重點是 postContext = continuation(); 最後是OnActionExecuted方法,結合上面的展開式,我們可以知道真正的調用順序將是:
複製代碼 代碼如下:

f3.Executing->f2.Executing->f1.Exectuing->InvokeActionMethod->f1.Executed->f2->Executed->f3.Executed.

那麼,原始碼中的注釋 // need to reverse the filter list because the continuations are built up backward 的意思也很明了了。需要將filter倒序排一下之後才是正確的執行順序。
還有一類filter是當異常發生的時候觸發的。在InvokeAction方法中可以看到觸發它的代碼放在一個catch塊中。IExceptionFilter的觸發流程比較簡單,不多做解釋了。唯一需要注意的是ExceptionHandled屬性設定為true的時候就不會拋出異常了,這個屬性在各種context下面都有,他們是的效果是一樣的。比如在OnActionExecuted方法中也可以將他設定為true,同樣不會拋出異常。這些都比較簡單,不再分析其原始碼,這篇文章比較詳細的介紹了filter流程中出現異常之後的執行順序。
最後說下Action Method的執行,前面我們已經得到了methodInfo,和通過data binding獲得了參數,調用Action Method應該是萬事俱備了。asp.net mvc這邊的處理還是比較複雜的,ReflectedActionDescriptor會去調用ActionMethodDispatcher的Execute方法,這個方法如下:
複製代碼 代碼如下:

public object Execute(ControllerBase controller, object[] parameters) {
return _executor(controller, parameters);
}

此處的_executor是
delegate object ActionExecutor(ControllerBase controller, object[] parameters);_exectuor被賦值是通過一個方法,利用Expression拼出方法體、參數,代碼在(ActionMethodDispatcher.cs):
static ActionExecutor GetExecutor(MethodInfo methodInfo)此處就不貼出了,比較複雜。這裡讓我比較費解的是,既然MethodInfo和parameters都有了,直接用反射就可以了,為什麼還要如此複雜,我將上面的Execute方法改為:
複製代碼 代碼如下:

public object Execute(ControllerBase controller, object[] parameters) {
return MethodInfo.Invoke(controller, parameters);
//return _executor(controller, parameters);
}

運行結果是完全一樣的。我相信mvc原始碼如此實現一定有其考慮,這個需要繼續研究。
最後附上一張函數調用圖,以便理解,僅供參考。圖片較大,點擊可看原圖。


相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.