ASP.NET MVC:Filter和Action的执行介绍_实用技巧

根据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源代码如此实现一定有其考虑,这个需要继续研究。
最后附上一张函数调用图,以便理解,仅供参考。图片较大,点击可看原图。

时间: 2024-09-15 10:37:36

ASP.NET MVC:Filter和Action的执行介绍_实用技巧的相关文章

ASP.NET MVC 4 捆绑和缩小实例介绍_实用技巧

在 ASP.NET MVC 4 中可捆绑多个 css 和 js 文件以减少HTTP请求,并对 css 和 js 文件进行压缩(缩小),这样可提高网站的加载速度.我们选取博客园的 css 文件来看看,在 ASP.NET MVC 4 前,我们引入 css 方法如下: 复制代码 代码如下: <link href="/Content/Site.css" rel="stylesheet" type="text/css" /> <link

asp.net MVC实现无组件上传图片实例介绍_实用技巧

例子: 如我想上传一个图片到服务器端:asp页面 复制代码 代码如下: <form id="form1" runat="server" action="/bookIndex/fileUpLoad/(你准备处理的 ActionResult)" method="post" enctype="multipart/form-data"> <input type="file" i

基于ASP.NET MVC的ABP框架入门学习教程_实用技巧

为什么使用ABP我们近几年陆续开发了一些Web应用和桌面应用,需求或简单或复杂,实现或优雅或丑陋.一个基本的事实是:我们只是积累了一些经验或提高了对,NET的熟悉程度. 随着软件开发经验的不断增加,我们发现其实很多工作都是重复机械的,而且随着软件复杂度的不断提升,以往依靠经验来完成一些简单的增删改查的做法已经行不通了.特别是用户的要求越来越高,希望添加的功能越来多,目前这种开发模式,已经捉襟见肘.我很难想象如何在现有的模式下进行多系统的持续集成并添加一些新的特性. 开发一个系统时,我们不可避免的

ASP.NET MVC自定义错误页面真的简单吗?_实用技巧

如果你在设置asp.net mvc自定义错误页面时遇到问题,这并不止你一个人.惊讶之余你的做法是正确的,没有起到作用的原因是其一部分错误是由asp.net管道处理的,另一部分是由iis直接处理. 通常情况 (我期望是这种情况,在一些其他框架/服务器上) 我们只需要在一个地方配置自定义错误页就可以了,无论怎么哪儿引发的错误.就像这样︰ <customErrors mode="On"> <error code="404" path="404.

ASP.NET MVC中的视图生成实例分析_实用技巧

本文实例分析了ASP.NET MVC中的视图生成过程.分享给大家供大家参考.具体如下: 在 ASP.NET MVC 中,我们将前端的呈现划分为三个独立的部分来实现,Controller 用来控制用户的操作,View 用来控制呈现的内容,Model 用来表示处理的数据. 从控制器到视图 通常,在 Controller 中,我们定义多个 Action ,每个 Action 的返回类型一般是 ActionResult,在 Action 处理的最后,我们返回对于视图的调用. 复制代码 代码如下: pub

ASP.NET MVC的四种验证编程方式_实用技巧

我们可以采用4种不同的编程模式来进行针对绑定参数的验证. 一.手工验证绑定的参数 在定义具体Action方法的时候,对已经成功绑定的参数实施手工验证无疑是一种最为直接的编程方式,接下来我们通过一个简单的实例来演示如何将参数验证逻辑实现在对应的Action方法中,并在没有通过验证的情况下将错误信息响应给客户端.我们在一个ASP.NET MVC应用中定义了如下一个Person类作为被验证的数据类型,它的Name.Gender和Age三个属性分别表示一个人的姓名.性别和年龄. public class

实现Asp.net mvc上传头像加剪裁功能_实用技巧

在我们使用QQ上传头像,注册用户账号时是不是都会遇到上传图像,并根据自己的要求对图像进行裁剪,这是怎么实现的呐? 本文主要介绍了Asp.net mvc实现上传头像加剪裁功能,分享给大家供大家参考.具体如下:  运行效果截图如下: 具体代码如下: 前台代码 <link href="~/Content/fineuploader.css" rel="stylesheet" /> <link href="~/Content/jquery.Jcro

ASP.NET MVC中使用JavaScriptResult的用法示例_实用技巧

本文实例讲述了ASP.NET MVC中使用JavaScriptResult的用法.分享给大家供大家参考,具体如下: 在页面中我们使用JavaScript来设置id为qubernet的span标签样式,具体代码如下: JS: <script> $(function () { var thisObj = document.getElementById("qubernet"); thisObj.setAttribute("style", "color

ASP.NET MVC文件上传教程(二)_实用技巧

上文ASP.NET MVC 文件上传教程(一)我们讲了简单的上传以及需要注意的地方,查相关资料时,感觉上传里面涉及到的内容还是比较多,于是就将上传这一块分为几节来处理,同时后续也会讲到关于做上传时遗漏的C#应该注意的地方,及时进行查漏补缺,尽量将这一块完善起来. 引入 上一节我们讲到了上传这一块,有朋友提出未涉及到大文件的上传这一块,思前想后还是来试着做做,毕竟之前没怎么去仔细考虑过这个问题,尤其还可以联系实际开发中创建文件夹等一系列问题,同时关于上传在网上随便找找都充斥着大量的组件,我们何必再