Filter是很好的实现crosscutting concern 的方式,常见的crosscutting concern包括log,验证,缓存,异常处理(笔者推荐postsharp),等等。
什么是crosscutting concern?
为了更好的专注业务的实现,降低耦合,提高内聚。因此把这些concern从业务中抽离出来解决,可以更好的维护,更改和扩展。它们也构成了架构中cross-cutting的部分,可以在与业务隔离的情况下,统一编码,测试,修改。
例如AdminController :
public class AdminController : Controller { // ...instance variables and constructor public ViewResult Index() { if(!Request.IsAuthenticated) { FormsAuthentication.RedirectToLoginPage(); } } public ViewResult Create() { if(!Request.IsAuthenticated) { FormsAuthentication.RedirectToLoginPage(); } } public ViewResult Edit(int productId) { if(!Request.IsAuthenticated) { FormsAuthentication.RedirectToLoginPage(); } } }
我们的目的很明确,我需要限制这个controller的访问,只有登录的才能访问,那么我们每个action都要加上Request.IsAuthenticated.如果需要限制role,指定用户才能访问,我们就要写一个验证函数,并修改全部的action。
[Authorize] public class AdminController : Controller { public ViewResult Index() { } public ViewResult Create() { } public ViewResult Edit(int productId) { } }
好处:可以看到filter提高了代码的可读性,减少了重复。
FilterAttribute的逻辑注入在http pipeline里面,在controller之前,反射出所有controller,查找customize的attribute,如果有加Authorize,调用相应的login验证函数(后续章节会介绍),验证不通过则返回401 。
Attribute是.net 中很好应用装饰模式的例子,通过继承自System.Attribute,可以实现自己的attribute,包括class,method,property,field。目的是动态的注入property,member,和ability到相应的类中,编译之后,生成的IL已经注入了attribute要提供的能力。通常的AOP框架中有attribute和IL wave 一起使用的代码和使用方法。不熟悉的读者可以查阅相关MSDN,本章所有内容都建立在attribute有一个基本的认识上的。
MVC中四种基本的filter
Authorize Filter |
验证。进入controller或action之前 |
Action Filter |
进入action前后会被调用 |
Result Filter |
Execute result的前后会被调用 |
Exception Filter |
Filter,action,或者execute result时发生异常时被触发 |
MVC中的authorizefilter除了可以加在class上(前面演示过了),还可以针对action做filter:
public class AdminController : Controller { [Authorize] public ViewResult Index() { } [Authorize] public ViewResult Create() { } }
[Authorize(Roles="trader")] public class ExampleController : Controller { [ShowMessage]// applies to just this action [OutputCache(Duration=60)]// applies to just this action public ActionResult Index() { // ...action method body } }
public class BlockAttribute : AuthorizeAttribute { public BlockAuthAttribute() { } protected override bool AuthorizeCore(HttpContextBase httpContext) { return false; } }
作为演示,实现了一个block attribute,加上这个attribute会block所有的request。现实场景中,拿到了httpContext可以做很多事情,因为里面的对象包含的信息非常丰富。
使用:
[Block] public string Index() { return "Always block "; }
访问页面会发现:
由于我创建的是basic的项目,没有加login view在Account,因此找不到login。但是filter已经生效了。
当使用AuthorizeAttribute,还有Users和Roles filter也很常用,使用方法:
[Authorize(Users= "iori", Roles = "admin")] public string Index() { return "This is the Index action on the Home controller"; }
这条filter就限制了访问这个action,需要user为iori,并且role为admin,关于MVC Authorize使用细节,如何完成验证,完成AccountController,会有专门一章介绍,在此只演示MVC有这样一个filter可以使用。
需要实现接口:
public interface IExceptionFilter { void OnException(ExceptionContextfilterContext); }
FilterContext对象包含:
Controller |
发生异常时的Controller Object |
HttpContext |
当前HttpContext对象 |
IsChildAction |
是否为子action |
RequestContext |
HttpContext对象 |
RouteData |
返回当前request的route数据 |
ActionDescriptor |
Action 方法的描述 |
Result |
Action method的result,可以修改result |
Exception |
异常对象 |
ExceptionHandled |
如果其他filter已经handle,则为true |
发生exception时候,拿到以上信息,除了记log,还可以做的事情:
页面跳转,跳转到错误页面
直接给客户端返回response
重写result返回给action
实现:
public class RangeExceptionAttribute : FilterAttribute, IExceptionFilter { public void OnException(ExceptionContext filterContext) { if (!filterContext.ExceptionHandled && filterContext.Exception is ArgumentOutOfRangeException) { filterContext.Result = new RedirectResult("~/Content/RangeErrorPage.html"); filterContext.ExceptionHandled = true; } } }
代码做的事情很简单,判断异常是否被handle,如果没有并且是argumentOutOfRange类型的exception,跳转到一个友好的错误页面。
错误页面代码:
<html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>Range Error</title> </head> <body> <h2>Sorry</h2> <span>One of the arguments was out of the expected range.</span> </body> </html>
使用这个Exception filter:
[RangeException] public ActionResult Index() { throw new ArgumentOutOfRangeException(); }
验证:
ExceptionType |
Exception type will be handled by this filter |
View |
View Name |
Master |
Layout Name |
Web config 配置 default redirect
<customErrors mode="On" defaultRedirect="/Content/RangeErrorPage.html"/>
[HandleError(ExceptionType= typeof(ArgumentOutOfRangeException), View = "RangeError")] public ActionResult Index() { throw new ArgumentOutOfRangeException(); }
RangeErrorView中会收到HandleErrorInfo 对象:
Action Name |
发生异常的action name |
Controller Name |
发生异常的controller name |
Exception |
Exception对象 |
View 代码:
@model HandleErrorInfo @{ ViewBag.Title= "Sorry, there was a problem!"; } <!DOCTYPEhtml> <html> <head> <meta name="viewport" content="width=device-width" /> <title>RangeError</title> </head> <body> <h2>Sorry</h2> <span>The value @(((ArgumentOutOfRangeException)Model.Exception).ActualValue) was out ofthe expected range.</span> <div> @Html.ActionLink("Change value and try again", "Index") </div> <div style="display: none"> @Model.Exception.StackTrace </div> </body> </html>
接口:
public interface IActionFilter { void OnActionExecuting(ActionExecutingContext filterContext); void OnActionExecuted(ActionExecutedContext filterContext); }
OnActionExecuting
在action执行前被调用
OnActionExecuted
在action执行完毕被调用(返回值之前)
CustomizeAction Attribute 实现
public class ActionTestAttribute : FilterAttribute, IActionFilter { public void OnActionExecuting(ActionExecutingContext filterContext) { filterContext.HttpContext.Response.Write("<br/>Written fromAction Filter.Should Before Execute action body"); } public void OnActionExecuted(ActionExecutedContext filterContext) { filterContext.HttpContext.Response.Write("<br/>Written fromAction filter,Should After Execute action body."); } }
目的很显然,在ActionExecuting和ActionExecuted分别打印一行字。
应用这个Filter
[ActionTest] public ActionResult Index() { Response.Write("<div>ActionBody</div>"); return Content("<br/>Action Result."); }
验证结果:
可以看到执行顺序:
·ActionFilter Executing
·Action body
·ActionFilter Executed
·Action body return result
参数常用对象
ActionExecutingContext对象:
Action Descriptor |
Action的描述 |
Result |
返回值 |
HttpContext |
当前的HttpContext对象 |
ActionExecutedContext稍有不同,我们能拿到更多的object
ActionDescriptor |
Action描述 |
Result |
Action Result |
Canceled |
如果action已经被(其他filter)cancel会被标记为true |
Exception |
异常对象 |
ExceptionHandled |
异常是否被handle了 |
HttpContext |
当前HttpContext对象 |
接口
public interface IResultFilter { void OnResultExecuting(ResultExecutingContext filterContext); void OnResultExecuted(ResultExecutedContext filterContext); }
代码实现:
public class ResultTestAttribute :FilterAttribute, IResultFilter { private Stopwatch timer; public void OnResultExecuting(ResultExecutingContext filterContext) { filterContext.HttpContext.Response.Write("<br/>----------StartExecuting Result.--------<br/>"); timer = Stopwatch.StartNew(); } public void OnResultExecuted(ResultExecutedContext filterContext) { timer.Stop(); filterContext.HttpContext.Response.Write( string.Format("<div>Result elapsed time:{0}</div>", timer.Elapsed.TotalSeconds)); } }
开始执行Result时,开启一个stop watch,在executed关闭,为了计时执行result用了多少时间,同时在executing和executed分别打印一行字。
应用这个filter:
[ResultTest] [ActionTest] public ActionResult Index() { Response.Write("<div>ActionBody</div>"); return Content("<br/>Return Action Result."); }
和刚才演示的actionfilter一起应用,正好看一下执行的顺序。
验证结果:
可见执行顺序:
2.ActionExecuting
3.Actionbody
4.ActionExecuted
5.ResultExecuting
6.Actionbody return result
7.Resultexecuted
使用自带的ActionFilterAttribute
ActionFilter实际提供了四个virtual的函数,都是刚刚提到的:
public virtual void OnActionExecuting(ActionExecutingContext filterContext) { } public virtual void OnActionExecuted(ActionExecutedContext filterContext) { } public virtual void OnResultExecuting(ResultExecutingContext filterContext) { } public virtual void OnResultExecuted(ResultExecutedContext filterContext) { }
继承这个类,根据实际场景的需要,我们可以选择性的override其中的几个
代码实现
public class ActionResultTestAttribute :ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContextfilterContext) { filterContext.HttpContext.Response.Write("<br />[ActionFilterUsage] Action Executing..."); } public override void OnResultExecuted(ResultExecutedContextfilterContext) { filterContext.HttpContext.Response.Write("<br />[ActionFilterUsage] Result Executed"); } }
以上代码演示了继承ActionFilterAttribute,重写OnActionExecuting和OnResultExecuted这两个函数,为了验证看的清楚,打印时加了特殊的标记。
使用:
[ActionResultTest] [ResultTest] [ActionTest] public ActionResult Index() { Response.Write("<div>Action Body</div>"); return Content("<br/>Return Action Result."); }
验证结果:
可以看到执行顺序:
·Action filter 中的Action Executing
·自定义的Action Executing
·Action Body
·自定义的Action Executed
·自定义的Result Executing
·Action Result
·自定义的Result Executed
·Action Filter中的Result Executed
给controller加Action,ResultExecuting, Executed
由于controller基类已经提供了virtual,因此直接重写就好了:
public class TestController : Controller { // // GET: /Test/ public ActionResult Index() { Response.Write("<div>Action Body</div>"); return Content("<br/>Return Action Result."); } protected override void OnActionExecuting(ActionExecutingContextfilterContext) { Response.Write("[From Controller ]: On action Executing."); } protected override void OnResultExecuted(ResultExecutedContextfilterContext) { Response.Write("[From Controller ] : On Result Executed."); } }
验证结果:
可以看到controller级别的filter生效了。虽然controller提供了virtual使得我们可以重写来完成controller级别的filter,但是仍然建议自定义或者使用默认的actionfilter。
1.打开App_Start 目录的filter config
让我们把刚才自定义的ActionTestFilter变成全局的:
public class FilterConfig { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(newHandleErrorAttribute()); filters.Add(newActionTestAttribute()); } }
这样一来,任何一个Action的执行,都会调用刚才定义的ActionTestFilter。
Action代码(记得拿掉ActionFilter以及刚才controller重写的):
public ActionResult Index() { Response.Write("<div>Action Body</div>"); return Content("<br/>Return Action Result."); }
验证:
可见,自定义filter已经成功被加在全局。
这个功能只针对Multiple Filter (也就是定义Attribute的时候,要AllowMultiple)
例子:
[AttributeUsage(AttributeTargets.Method,AllowMultiple = true)] public class SomeMessageAttribute :FilterAttribute, IActionFilter { public string Message { get; set; } public void OnActionExecuting(ActionExecutingContext filterContext) { filterContext.HttpContext.Response.Write( string.Format("<div>[Before Action] [Message: <{0}>]<div>", Message)); } public void OnActionExecuted(ActionExecutedContext filterContext) { filterContext.HttpContext.Response.Write( string.Format("<div>[After Action] [Message: <{0}>]<div>", Message)); } }
代码说明:在executing和executed分别打印出message。
应用这个filter(为了分开演示清楚,记得把刚才全局filter拿掉):
[SomeMessage(Message = "A",Order =1)] [SomeMessage(Message = "B",Order = 2)] [SomeMessage(Message = "C",Order = 3)] public ActionResult Index() { Response.Write("<div>Action Body</div>"); return Content("<br/>Return Action Result."); }
我们加了三个filter,并安排了执行顺序。
查看结果:
可见按着我们期望的结果,现在调整一下顺序:
[SomeMessage(Message = "A",Order = 3)] [SomeMessage(Message = "B",Order = 1)] [SomeMessage(Message = "C",Order = 2)]
验证结果:
期望结果是B – C – A
查看结果:
RequireHttps:限制只接受https协议
OutputCache : 和Web Form中的使用方法类似,传递一个duration,就可以享受页面缓存了
Post:限制请求类型必须为post
Get:限制请求类型必须为Get