使用Filter
Filter是很好的实现crosscutting concern 的方式,常见的crosscutting concern包括log,验证,缓存,异常处理(笔者推荐postsharp),等等。
什么是crosscutting concern?
为了更好的专注业务的实现,降低耦合,提高内聚。因此把这些concern从业务中抽离出来解决,可以更好的维护,更改和扩展。它们也构成了架构中cross-cutting的部分,可以在与业务隔离的情况下,统一编码,测试,修改。
言归正传--Filter
例如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。
应用了Filter的实现
[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
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时发生异常时被触发 |
给controller和action加 Filter:
MVC中的authorizefilter除了可以加在class上(前面演示过了),还可以针对action做filter:
public class AdminController : Controller {
[Authorize]
public ViewResult Index() {
}
[Authorize]
public ViewResult Create() {
}
}
也可以apply多个filter:
[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
}
}
Customize filter
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可以使用。
Exception Filter
Customize Exception 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();
}
验证:
使用自带的Exception Filter – HandleErrorAttribute
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"/>
Apply filter
[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>
Action filter
接口:
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对象 |
Result Filter
接口
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。
使用全局filter
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已经成功被加在全局。
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
查看结果:
其他一些常用filter
RequireHttps:限制只接受https协议
OutputCache : 和Web Form中的使用方法类似,传递一个duration,就可以享受页面缓存了
Post:限制请求类型必须为post
Get:限制请求类型必须为Get