ASP.Net MVC探索之路 - 不想在多个Action上写同样的FitlerAttribute(上)

(写完本文后,我去下载了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
<stringobject> 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 ,它实现了本章提到的一部分的需求。
posted @ 2010-12-08 18:47  alby  阅读(3171)  评论(10编辑  收藏  举报