Asp.Net MVC 系列--進階篇之Filter

mybwu_com發表於2014-04-08

使用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

相關文章