過濾器(Filters)
過濾器(Filters)向請求處理管道注入了額外的邏輯。他們提供了一種簡單而優雅的方式實現了橫切關注點,這個術語是針對整個應用程序使用的功能,並不能靈活的適用任何一個點,所以這個會打破分解關注點的模式。像日志,驗證和緩存都是經典的橫切關注點的例子。
之所以稱為過濾器(Filters),是因為這個術語同樣應用於其他web應用程序框架里面,包括Ruby on Rails。然而,MVC框架里面的過濾器完全不同於ASP.NET平台里面的Request.Filters和Response.Filter對象,這兩個對象是實現請求和響應流的傳輸(一種高級的並很少發生的活動)。當然,我們能夠在MVC框架里面使用這兩個對象,但通常我們談及過濾器是指MVC框架里面的過濾器。本章里面會介紹MVC框架所支持的不同的過濾器策略以及如何控制它們執行。
使用Filters
在前面的SportsStore項目里面已經使用了過濾,就是將驗證應用到了Controller的action方法里面,只有驗證的用戶才能請求相應的action方法,這為我們提供了一種選擇的途徑。我們可以在每一個action方法里面檢查請求的驗證狀態。如下所示:

namespace SportsStore.WebUI.Controllers {
public class AdminController : Controller {
// ...
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();
}
// ...
}
// ...
}
}
通過上面的代碼,我們可以發現其實里面有很多重復的地方,那使用Filters就能解決這個問題。如下代碼所示:

namespace SportsStore.WebUI.Controllers {
[Authorize]
public class AdminController : Controller {
// ...
public ViewResult Index() {
// ...
}
public ViewResult Create() {
// ...
}
public ViewResult Edit(int productId) {
// ...
}
// ...
}
}
Filters是.NET里面的特性(Attributes),這些是添加到請求處理管道額外的步驟。上面使用了Authorize過濾器實現同樣的效果,但是代碼顯然更加簡捷優雅。有四種基本的過濾器類型:Authorization, Action, Result, Exception對應接口為IAuthorizationFilter,IActionFilter,IResultFilter,IExceptionFilter。
在MVC框架調用一個Action之前,它會檢查方法的定義中是否有實現了上面接口的特性(Attributes).如果有的話,那么在請求處理管道適當的位置,通過該接口定義的方法會被調用。MVC框架包含了默認已經實現了過濾器接口的Attributes類。
注:ActionFilterAttribute類實現了IActionFilter和IResultFilter接口,這個類是抽象類。其他的像AuthorizeAttribute和HandleErrorAttribute,包含了有用的功能,不需要派生出一個類就可以直接使用。
對Controllers和Action方法應用過濾器
Filters可以應用到單個的Action方法或者是整個Controller類,如下所示:

namespace SportsStore.WebUI.Controllers {
public class AdminController : Controller {
// ... instance variables and constructor
[Authorize]
public ViewResult Index() {
if (!Request.IsAuthenticated) {
FormsAuthentication.RedirectToLoginPage();
}
// ...rest of action method
}
[Authorize]
public ViewResult Create() {
if (!Request.IsAuthenticated) {
FormsAuthentication.RedirectToLoginPage();
}
// ...rest of action method
}
// ... other action methods
}
}
我們可以使用多個Filters,也可以混合使用Filters。如下所示:

[Authorize(Roles="trader")] // applies to all actions
public class ExampleController : Controller {
[ShowMessage] // applies to just this action
[OutputCache(Duration=60)] // applies to just this action
public ActionResult Index() {
// ... action method body
}
}
有一些過濾器獲取幾個參數,后面會有探究其原理。
注:如果我們自定義了針對Controllers的基類,任何應用在基類的過濾器會對其所有的派生類起作用。
使用Authorization過濾器
Authorization過濾器是在其他的過濾器之前運行,並且也是在Action方法被調用之前。這個過濾器是用來授權認證的,確保只有授權的用戶才能調用。看一下這樣一個場景,MVC框架接收一個來自瀏覽器的請求,路由系統處理請求的URL並提取被命中的controller和action的名字。一個新的controller實例被創建,但是在action方法被調用之前,MVC框架會檢查是否有應用到該action方法上面的授權認證過濾器(authorization filters)。如果有,那么定義在IAuthorizationFilter接口里面的唯一的方法OnAuthorization會被調用。如果authentication filter批准了該請求,那么請求管道里面的下一步就會被執行,否則請求就會被拒絕。
創建一個授權認證過濾器(Creating an Authentication Filter)
下面是一個檢查用戶是否登錄的過濾器,如下所示:

namespace MvcFilters.Infrastructure.Filters
{
public class CustomAuthAttribute : AuthorizeAttribute
{
private string[] allowedUsers;
public CustomAuthAttribute(params string[] users)
{
allowedUsers = users;
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
return httpContext.Request.IsAuthenticated &&
allowedUsers.Contains(httpContext.User.Identity.Name,
StringComparer.InvariantCultureIgnoreCase);
}
}
}
我們定義的過濾器需要一個數組參數,數組里面包含了被授權認證的用戶,我們的過濾器里面包含了PerformAuthenticationCheck方法,確保請求是被認證的並用戶也是在授權的集合里面的。這個類里面最有趣的部分是OnAuthorization方法的實現,傳遞給該方法的參數是AuthorizationContext類的實例,這個類是從ControllerContext派生。這讓我們可以訪問一些有用的對象,這些對象包含:Controller,HttpContext,IsChildAction,RequestContext,RouteData。
回想下,當我們判斷請求是否通過驗證用到的方法:filterContext.HttpContext.Request.IsAuthenticated。使用Context對象,我們能獲取需要對請求做決斷的所有信息。
AuthorizationContext定義了兩個額外的屬性:
1.ActionDescriptor(類型是ActionDescriptor)提供Action方法的詳情
2.Result(類型是ActionResult)Action方法的返回結果,是一個通過設置這個屬性值為non-null來取消請求的過濾器
第一個屬性ActionDescriptor返回的是System.Web.Mvc.ActionDescriptor類的實例,我們能夠用來獲取應用了過濾器的action方法的有關信息。
第二個屬性Result,是讓過濾器工作的關鍵。當我們給一個針對action請求授權,然后我們在OnAuthorization里面什么都不做,這會讓MVC框架默認請求應該被處理。然而如果我們設置Result屬性為一個ActionResult對象,MVC框架會使用它作為所有請求的結果,在請求管道里剩余的步驟也不會執行,並且我們提供的結果會為用戶生成輸出。
我們的例子里面,如果PerformAuthenticationCheck返回false(表明請求沒有被驗證或用戶沒有被授權),那么我們創建一個HttpUnauthorizedResult結果並賦給Result屬性,如:filterContext.Result = new HttpUnauthorizedResult();
使用我們自定義的過濾器如下:

...
[CustomAuth("adam", "steve", "bob")]
public ActionResult Index() {
return View();
}
...
使用內置的授權過濾器(Using the Built-in Authorization Filter)
MVC框架里面包含了一個非常有用的內置的授權過濾器稱為AuthorizeAttribute。我們可以具體通過兩個屬性來設置驗證策略。
兩個公開的屬性:1.Users(類型String) 2.Roles(類型String)。示例代碼如下:

...
[Authorize(Users="adam, steve, bob", Roles="admin")]
public ActionResult Index()
{
return View();
}
...
上面的過濾器表示只有用戶是 adam, steve, 和bob的,並且用戶有管理員權限的才能請求該action方法。
對大多數應用程序,AuthorizeAttribute提供的驗證策略已經足夠使用。如果我們想實現比較特殊的過濾器,可以從這個類派生。相對於直接實現IAuthorizationFilter接口的方式,這種風險更低。AuthorizeAttribute類提供兩個可以自己現實的點。分別如下:
1.AuthorizeCore方法:被OnAuthorization的AuthorizeAttribute實現調用以及實現授權檢查
2.HandleUnauthorizedRequest方法:當授權檢查失敗時調用
注:我們能夠重寫的第三個方法是OnAuthorization,推薦不要這樣做。因為這個方法默認的實現包含了對內容緩存使用輸出緩存過濾器的安全地處理。后面會介紹
實現一個自定義授權策略
下面創建一個自己的AuthorizeAttribute子類。這個策略將要授權任何從服務器桌面直接運行瀏覽器(Request.IsLocal為true),或者遠程用戶的用戶名能夠匹配normalAuthorizeAttribute的規則。這對於服務器管理員繞開網站的登錄過程非常有用。代碼實現如下:

using System.Web;
using System.Web.Mvc;
namespace MvcFilters.Infrastructure.Filters
{
public class OrAuthorizationAttribute : AuthorizeAttribute
{
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
return httpContext.Request.IsLocal || base.AuthorizeCore(httpContext);
}
}
}
可以這樣使用這個過濾器:[OrAuthorization(Users = "adam, steve, bob", Roles = "admin")]
實現一個自定義的授權失敗策略
默認的處理為通過授權的做法是重定向用戶到登錄頁面。但是我不想一直這么做,例如,如果我們使用AJAX,發送一個重定向能讓登錄頁出現在當前頁面的中間(類似模態窗口這樣的效果)。這時我們可以重寫HandleUnauthorizedRequest方法來實現,如下所示:

using System.Web.Mvc;
namespace MvcFilters.Infrastructure.Filters {
public class AjaxAuthorizeAttribute : AuthorizeAttribute {
protected override void HandleUnauthorizedRequest(AuthorizationContext context) {
if (context.HttpContext.Request.IsAjaxRequest()) {
UrlHelper urlHelper = new UrlHelper(context.RequestContext);
context.Result = new JsonResult {
Data = new {
Error = "NotAuthorized",
LogOnUrl = urlHelper.Action("LogOn", "Account")
}, JsonRequestBehavior = JsonRequestBehavior.AllowGet};
} else {
base.HandleUnauthorizedRequest(context);
}
}
}
}
使用異常過濾器(Using Exception Filters)
異常過濾器是在調用一個action方法拋出了無法處理的異常時運行。
創建一個異常過濾器(Creating an Exception Filter)
異常過濾器必須實現IExceptionFilter接口:public interface IExceptionFilter {void OnException(ExceptionContext filterContext);} OnException方法在異常拋出的時候調用。參數是ExceptionContext對象。下面是一個示例:

using System.Web.Mvc;
using System;
namespace MvcFilters.Infrastructure.Filters {
public class MyExceptionAttribute: FilterAttribute, IExceptionFilter {
public void OnException(ExceptionContext filterContext) {
if (!filterContext.ExceptionHandled &&
filterContext.Exception is NullReferenceException) {
filterContext.Result = new RedirectResult("/SpecialErrorPage.html");
filterContext.ExceptionHandled = true;
}
}
}
}
這個過濾器響應NullReferenceException異常的實例,並在沒有其他異常過濾器顯示異常被處理的情況下執行。我們重定向用戶到錯誤頁面。
使用內置的異常過濾器
HandleErrorAttribute就是內置的實現了IExceptionFilter接口的過濾器,並是創建異常過濾器變得容易。
當一個未處理的並指定了ExceptionType的異常發生時,過濾器會設置HTTP結果代碼為500(服務器錯誤),並呈現在View屬性里面指定的視圖,如下所示:
[HandleError(ExceptionType=typeof(NullReferenceException), View="SpecialError")]
注意:HandleErrorAttribute只有在web.config文件里面添加了<customErrors mode="On" />才能工作。默認的為RemoteOnly,這意味着在開發過程中HandleErrorAttribute不會攔截異常,當部署到服務器並從另外的計算機發送請求是才起作用。
當呈現一個視圖時,HandleErrorAttribute過濾器會傳遞一個HandleErrorInfo視圖模型對象,如下所示:

@Model HandleErrorInfo
@{
ViewBag.Title = "Sorry, there was a problem!";
}
<p>
There was a <b>@Model.Exception.GetType().Name</b>
while rendering <b>@Model.ControllerName</b>'s
<b>@Model.ActionName</b> action.
</p>
<p>
The exception message is: <b><@Model.Exception.Message></b>
</p>
<p>Stack trace:</p>
<pre>@Model.Exception.StackTrace</pre>
使用Action和Result過濾器
Action和Result過濾器是過多用途的過濾器,可用於任何意圖。為了創建這些過濾器類型的內置類,IActionFilter,實現了以上接口。如下:

namespace System.Web.Mvc {
public interface IActionFilter {
void OnActionExecuting(ActionExecutingContext filterContext);
void OnActionExecuted(ActionExecutedContext filterContext);
}
}
接口定義了兩個方法,MVC框架在調用Action方法之前調用OnActionExecuting方法,OnActionExecuted方法則是在Action方法調用完成后被調用。
實現OnActionExecuting方法
OnActionExecuting方法在Action方法之前調用,所以,我們可以利用這個機會來檢查請求,取消請求,修改請求,或者啟用一些活動來跨越action方法。參數類型ActionExecutingContext是ControllerContext的子類並定義了兩個屬性:ActionDescriptor和Result。我們可以通過Result屬性選擇性的取消請求,如下所示:

using System.Web.Mvc;
namespace MvcFilters.Infrastructure.Filters {
public class MyActionFilterAttribute : FilterAttribute, IActionFilter {
public void OnActionExecuting(ActionExecutingContext filterContext) {
if (!filterContext.HttpContext.Request.IsSecureConnection) {
filterContext.Result = new HttpNotFoundResult();
}
}
public void OnActionExecuted(ActionExecutedContext filterContext) {
// do nothing
}
}
}
上面的例子用來檢查請求是否使用了SSL,如果沒有則返回404.
實現OnActionExecuted方法
我們也可以使用這個過濾器來執行一些跨越action方法執行的任務,下面的示例計算action方法執行的時間,如下:

using System.Diagnostics;
using System.Web.Mvc;
namespace MvcFilters.Infrastructure.Filters {
public class ProfileAttribute : FilterAttribute, IActionFilter {
private Stopwatch timer;
public void OnActionExecuting(ActionExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}
public void OnActionExecuted(ActionExecutedContext filterContext) {
timer.Stop();
if (filterContext.Exception == null) {
filterContext.HttpContext.Response.Write(
string.Format("Action method elapsed time: {0}",
timer.Elapsed.TotalSeconds));
}
}
}
}
實現結果過濾器(Implementing a Result Filter)
Action filters和Result filters有很多共同點。它要實現IResultFilter接口,如下:

namespace System.Web.Mvc {
public interface IResultFilter {
void OnResultExecuting(ResultExecutingContext filterContext);
void OnResultExecuted(ResultExecutedContext filterContext);
}
}
一旦action方法返回了一個action result,OnResultExecuting方法就會被調用,但這是在action result執行之前,OnResultExecuted方法是在action result執行之后。
示例如下:

using System.Diagnostics;
using System.Web.Mvc;
namespace MvcFilters.Infrastructure.Filters {
public class ProfileResultAttribute : FilterAttribute, IResultFilter {
private Stopwatch timer;
public void OnResultExecuting(ResultExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}
public void OnResultExecuted(ResultExecutedContext filterContext) {
timer.Stop();
filterContext.HttpContext.Response.Write(
string.Format("Result execution - elapsed time: {0}",
timer.Elapsed.TotalSeconds));
}
}
}
使用內置的Action和Result過濾器類
MVC框架包含了能夠創建action和result過濾器的內置類,但是沒有提供任何有用的功能,這個ActionFilterAttribute類如下所示:

public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter{
public virtual void OnActionExecuting(ActionExecutingContext filterContext) {
}
public virtual void OnActionExecuted(ActionExecutedContext filterContext) {
}
public virtual void OnResultExecuting(ResultExecutingContext filterContext) {
}
public virtual void OnResultExecuted(ResultExecutedContext filterContext) {
}
}
}
下面是一個從它派生的過濾器,如下:

using System.Diagnostics;
using System.Web.Mvc;
namespace MvcFilters.Infrastructure.Filters {
public class ProfileAllAttribute : ActionFilterAttribute {
private Stopwatch timer;
public override void OnActionExecuting(ActionExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}
public override void OnActionExecuted(ActionExecutedContext filterContext) {
timer.Stop();
filterContext.HttpContext.Response.Write(
string.Format("Action method elapsed time: {0}",
timer.Elapsed.TotalSeconds));
}
public override void OnResultExecuting(ResultExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}
public override void OnResultExecuted(ResultExecutedContext filterContext) {
timer.Stop();
filterContext.HttpContext.Response.Write(
string.Format("Action result elapsed time: {0}",
timer.Elapsed.TotalSeconds));
}
}
}
使用其他過濾功能
除了上面介紹的過濾器以外,還有一些非常有趣但是用的並不那么廣泛的過濾功能。如下:
不使用特性的過濾
使用過濾器常規的方式是創建和使用特性(attributes),然而有一種可以替代的方式。Controller類實現IAuthorizationFilter, IActionFilter, IResultFilter, IExceptionFilter這些接口,同時也提供了每一種OnXXX方法的虛擬實現。下面是計算action方法執行時間的實現,如下:

using System.Diagnostics;
using System.Web.Mvc;
namespace MvcFilters.Controllers {
public class SampleController : Controller {
private Stopwatch timer;
public ActionResult Index() {
return View();
}
protected override void OnActionExecuting(ActionExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}
protected override void OnActionExecuted(ActionExecutedContext filterContext) {
timer.Stop();
filterContext.HttpContext.Response.Write(
string.Format("Action method elapsed time: {0}",
timer.Elapsed.TotalSeconds));
}
protected override void OnResultExecuting(ResultExecutingContext filterContext) {
timer = Stopwatch.StartNew();
}
protected override void OnResultExecuted(ResultExecutedContext filterContext) {
timer.Stop();
filterContext.HttpContext.Response.Write(
string.Format("Action result elapsed time: {0}",
timer.Elapsed.TotalSeconds));
}
}
}
這種技術在我們創建一個基類,該基類從項目里面多個控制器派生出來。過濾的重點就是把跨應用程序所需的代碼放在一個可重復使用的位置。對我們的項目而言,我們寧願使用特性。這樣能夠將控制邏輯和過濾邏輯分開。
使用全局過濾
全局過濾是應用到所有的action方法,通過如下的方式實現:
public class MvcApplication : System.Web.HttpApplication {
public static void RegisterGlobalFilters(GlobalFilterCollection filters)//在Application_Start里面調用
{
filters.Add(new HandleErrorAttribute());
filters.Add(new ProfileAllAttribute()); //注意這里一定是完整的類名,不能是ProfileAll。一旦我們在這里注冊了過濾器,將應用與所有的action
}
...
過濾執行排序
前面解釋了過濾器按類型被執行,順序是:authorization過濾器,action過濾器,result過濾器。如果在執行的任何一步出現未處理的異常,異常過濾器都會被執行。然而在每一個類型里面,我們能夠控制個別的過濾器的順序。如下:

using System;
using System.Web.Mvc;
namespace MvcFilters.Infrastructure.Filters {
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple=true)]
public class SimpleMessageAttribute : FilterAttribute, IActionFilter {
public string Message { get; set; }
public void OnActionExecuting(ActionExecutingContext filterContext) {
filterContext.HttpContext.Response.Write(
string.Format("[Before Action: {0}]", Message));
}
public void OnActionExecuted(ActionExecutedContext filterContext) {
filterContext.HttpContext.Response.Write(
string.Format("[After Action: {0}]", Message));
}
}
}
在action里面使用如下:

...
[SimpleMessage(Message="A")]
[SimpleMessage(Message="B")]
public ActionResult Index() {
Response.Write("Action method is running");
return View();
}
...
運行結果如下:
如果我們要指定順序可以這樣:

...
[SimpleMessage(Message="A", Order=2)]
[SimpleMessage(Message="B", Order=1)]
public ActionResult Index() {
Response.Write("Action method is running");
return View();
}
...
運行程序如下:
如果我們不給order指定一個具體的值,默認會是-1。如果我們混淆了過濾器以至於一些有其他的值,一些沒有,這些沒有值的將會首先被執行。如果多個filters有同樣的值,那么MVC框架根據它們被使用的地方來決定執行順序。全局過濾器最先執行,然后應用在controller類上面的,最后是action上面的過濾器執行。
使用內置的過濾器
MVC框架提供了一些內置的過濾器,以備我們在程序里面直接使用。如下所示:
好了,本章的筆記到這就結束了,祝大家愉快!