閱讀654 返回首頁    go 技術社區[雲棲]


深入探討ASP.NET MVC的篩選器

在ActionInvoker對Action的執行過程中,除了通過利用ActionDescriptor對Action方法的執行,以及之前進行的Model綁定與驗證之外,還具有一個重要的工作,那就是對相關篩選器(Filter)的執行。ASP.NET MVC的篩選器是一種基於AOP(麵向方麵編程)的設計,我們將一些非業務的邏輯實現在相應的篩選器中,然後以一種橫切(Crosscutting)的方式應用到對應的Action方法。當Action方法執行前後,這些篩選器會自動執行。ASP.NET MVC提供了四種類型的篩選器(AuthorizationFilter、ActionFilter、ResultFilter和ExceptionFilter),它們對應著相應的篩選器接口(IAuthorizationFilter、IActionFilter、IResultFilter和IExceptionFilter)。[本文已經同步到《How ASP.NET MVC Works?》中]

目錄
一、Filter
二、FilterProvider
三、FilterAttribute與FilterAttributeFilterProvider
四、Controller與ControllerInstanceFilterProvider
五、GlobalFilterCollection
六、實例演示:驗證Filter的提供機製和執行順序

雖然ASP.NET MVC提供的四種類型的篩選器具有各自實現的接口,但是對於篩選器的提供體係來說所有的篩選器都通過具有如下定義的Filter類型表示。Filter的核心是Instance屬性,因為它代表真正實施篩選功能的對象,該對象實現了一個或者多個基於上述四種篩選器類型的接口。

   1: public class Filter
   2: {    
   3:     public const int DefaultOrder = -1;   
   4:     public Filter(object instance, FilterScope scope, int? order);
   5:     
   6:     public object Instance { get; protected set; }
   7:     public int Order { get; protected set; }
   8:     public FilterScope Scope { get; protected set; }
   9: }
  10:  
  11: public enum FilterScope
  12: {
  13:     Action        = 30,
  14:     Controller    = 20,
  15:     First         = 0,
  16:     Global        = 10,
  17:     Last          = 100
  18: }

注:由於System.Web.Mvc.Filter和實現了IAuthorizationFilter、IActionFilter、IResultFilter和IExceptionFilter的類型均可以被稱為“篩選器”,為了不至於造成混淆,在沒有做明確說明的情況下,我們使用英文“Filter”和中文“篩選器”分別來表示它們。

Filter的Order和Scope屬性最終決定了篩選器的執行順序。Order屬性對應數值越小,執行的優先級越高,該屬性的默認值為-1(對應著Filter中定義的常量DefaultOrder)。如果兩個Filter具有相同的Order屬性值,那麼Scope屬性最終決定哪個被優先執行。Filter的Scope屬性類型是一個類型為FilterScope的枚舉。該枚舉表示應用Filter的範圍,Action和Controller代表Action方法和Controller類級別;First和Last意味著希望被作為第一個和最後一個Filter來執行;Global代表一個全局的Filter。

通過上麵的代碼片斷我們可以看到FilterScope的5個枚舉選項均被設置了一個值,這個值決定了Filter的執行順序,具有更小的枚舉值會被優先執行。從FilterScope的定義可以得到這樣的結論:。

Filter的提供機製與之前我們介紹的基於ModelBinder和ModelValidator的提供機製比較類似,均是通過相應的Provider來提供的。提供篩選器的FilterProvider實現了接口IFilterProvider,如下麵的代碼片斷所示,該接口定義了唯一的方法GetFilters根據指定的Controller上下文和用於描述目標Action的ActionDescriptor對象獲取一個Filter對象集合。

   1: public interface IFilterProvider
   2: {   
   3:     IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor);
   4: }

我們可以通過靜態類型FilterProviders注冊或者獲取當前應用使用的FilterProvider。如下麵的代碼片斷所示,FilterProviders具有一個類型為FilterProviderCollection的隻讀屬性Providers,表示基於整個Web應用範圍內被使用的FilterProvider列表。FilterProviderCollection是元素類型為IFilterProvider的集合,GetFilters方法用於或者該集合中所有FilterProvider對象提供的Filter對象。

   1: public static class FilterProviders
   2: {    
   3:     public static FilterProviderCollection Providers { get; }
   4: }
   5:  
   6: public class FilterProviderCollection : Collection<IFilterProvider>
   7: {   
   8:     
   9:     //其他成員
  10:     public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor);   
  11: }

ASP.NET MVC提供了三種原生的FilterProvider,分別是FilterAttributeFilterProvider、ControllerInstanceFilterProvider和GlobalFilterCollection,接下來我們對它們進行單獨介紹。

我們通常將篩選器定義成特性以聲明的方式應用到Controller類型或者Action方法上,而抽象類型FilterAttribute是所有篩選器的基類。如下麵的代碼片斷所示,FilterAttribute特性實現了IMvcFilter接口,該接口定義了Order和AllowMultiple兩個隻讀屬性,分別用於控製篩選器的執行順序以及多個同類的篩選器能夠同時應用到同一個目標元素(類或者方法)。

   1: [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited=true, AllowMultiple=false)]
   2: public abstract class FilterAttribute : Attribute, IMvcFilter
   3: {    
   4:     protected FilterAttribute();
   5:     
   6:     public bool AllowMultiple { get; }
   7:     public int Order { get; set; }
   8: }
   9:  
  10: public interface IMvcFilter
  11: {    
  12:     bool AllowMultiple { get; }
  13:     int Order { get; }
  14: }

從應用在FilterAttribute上的AttributeUsageAttribute的定義可以看出該特性可以應用在類型和方法上,這意味著篩選器一般都可以應用在Controller類型和Action方法上。隻讀屬性AllowMultiple實際上返回的是AttributeUsageAttribute的同名屬性,通過上麵的定義我們可以看到默認情況下該屬性值為False。

用於描述Controller和Action的ControllerDescriptor和ActionDescriptor均實現了ICustomAttributeProvider接口,我們可以直接利用它們獲取應用在對應的Controller類型或者Action方法上包括FilterAttribute在內的所有特性。實際上,這兩個描述類型提供了單獨的方法GetFilterAttributes專門用於獲取FilterAttribute特性列表。如下麵的代碼片斷所示,該方法具有一個布爾類型的參數useCache,表示是否需要對解析出來的FilterAttribute特性進行緩存以緩解頻繁的反射操作對性能造成的影響。

   1: public abstract class ControllerDescriptor : ICustomAttributeProvider, IUniquelyIdentifiable
   2: {
   3:     //其他成員
   4:     public virtual IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache);
   5: }
   6: public abstract class ActionDescriptor : ICustomAttributeProvider, IUniquelyIdentifiable
   7: {   
   8:     //其他成員
   9:     public virtual IEnumerable<FilterAttribute> GetFilterAttributes(bool useCache);    
  10: }

針對FilterAttribute特性的Filter通過FilterAttributeFilterProvider對象來提供。FilterAttributeFilterProvider直接調用當前ControllerDescriptor和ActionDescriptor的GetFilterAttributes方法獲取所有應用在Controller類型和當前Action方法的FilterAttribute特性,並借此創建相應的Filter對象。FilterAttributeFilterProvider構造函數的參數cacheAttributeInstances表示是否啟用針對FilterAttribute的緩存,它將作為調用GetFilterAttributes方法的參數。在默認的情況下(通過調用默認無參的構造函數創建的FilterAttributeFilterProvider)會采用針對FilterAttribute的緩存。

   1: public class FilterAttributeFilterProvider : IFilterProvider
   2: {
   3:     public FilterAttributeFilterProvider();
   4:     public FilterAttributeFilterProvider(bool cacheAttributeInstances);
   5:     protected virtual IEnumerable<FilterAttribute> GetActionAttributes(ControllerContext controllerContext, ActionDescriptor actionDescriptor);
   6:     protected virtual IEnumerable<FilterAttribute> GetControllerAttributes(ControllerContext controllerContext, ActionDescriptor actionDescriptor);
   7:     public virtual IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor);
   8: }

對於通過調用GetFilters得到的Filter,對應的FilterAttribute特性作為其Instance屬性。Order屬性來源於FilterAttribute的同名屬性,而Scope屬性則取決於FilterAttribute特性是應用在Controller類型上(Scope屬性值為Controller)還是當前的Action方法上(Scope屬性值為Action)。

提到ASP.NET MVC的篩選器,大部分的都隻會想到通過FilterAttribute特性,實際上Controller本身(繼承自抽象類Controller)就是一個篩選器。如下麵的代碼片斷所示,抽象類Controller實現了IActionFilter、IAuthorizationFilter、IExceptionFilter和IResultFilter這四個對應著不同篩選器類型的接口。

   1: public abstract class Controller : ControllerBase, 
   2:     IActionFilter, 
   3:     IAuthorizationFilter, 
   4:     IExceptionFilter, 
   5:     IResultFilter, 
   6:      ...
   7: {
   8:    //省略成員
   9: }

針對Controller對象這種獨特篩選器的FilterProvider類型為具有如下定義的ControllerInstanceFilterProvider。在實現的GetFilters方法中,它會根據指定的Controller上下文獲取對應的Controller對象,並以此創建一個Filter(Controller對象作為Filter對象的Instance屬性值)。該Filter的Scope不是Controller,而是First,而Order的值為-2147483648(Int32.MinValue),毫無疑問。

   1: public class ControllerInstanceFilterProvider : IFilterProvider
   2: {    
   3:     public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor);   
   4: }

通過FilterAttribute的形式定義的篩選器需要顯式地標注到目標Controller類型或者Action方法上,而在有些情況下需要一種全局的Filter。所謂全局篩選器,就是不需要顯式與某個Controller或者Action進行匹配,而是默認使用到所有的Action執行過程中。用於提供這種全局Filter的FilterProvider對應的類型為具有如下定義的GlobalFilterCollection

   1: public sealed class GlobalFilterCollection : IEnumerable<Filter>, IEnumerable, IFilterProvider
   2: {
   3:     public GlobalFilterCollection();
   4:     public void Add(object filter);
   5:     public void Add(object filter, int order);
   6:     private void AddInternal(object filter, int? order);
   7:     public void Clear();
   8:     public bool Contains(object filter);
   9:     public IEnumerator<Filter> GetEnumerator();
  10:     public void Remove(object filter);
  11:     IEnumerator IEnumerable.GetEnumerator();
  12:     IEnumerable<Filter> IFilterProvider.GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor);
  13:  
  14:     public int Count { get; }
  15: }

通過命名以及上麵給出的定義可以看出GlobalFilterCollection就是一個Filter的列表而已,實現的GetFilters方法返回的就是它自己。通過GlobalFilterCollection提供的方法我們可以實現對全局Filter的添加、刪除和清除操作。用於添加Filter的Add方法的參數filter不是一個Filter對象,而是一個具體篩選器(實現了相應的篩選器接口),添加的Filter對象根據該篩選器對象創建,其Scope屬性被設置成。我們通過在Add方法指定添加Filter對象的Order屬性,如果沒有顯示指定Order並且指定的篩選器是一個FilterAttribute特性,那麼該特性的Order將會作為Filter對象的Order;否則使用-1作為Order屬性值。

針對整個Web應用的全局Filter(或者說全局FilterProvider)的注冊和獲取可以通過靜態類型GlobalFilters來實現。如下麵的代碼片斷所示,GlobalFilters具有一個靜態隻讀屬性Filters返回一個GlobalFilterCollection對象。

   1: public static class GlobalFilters
   2: {    
   3:     public static GlobalFilterCollection Filters { get; }
   4: }

到目前為止,我們已經介紹了ASP.NET MVC默認提供的三種FilterProvider,以及各自采用得Filter提供機製。當用於注冊FilterProvider的靜態類型在加載的時候,會默認創建這三種類型的對象並將其作為表示全局FilterProvider集合的Providers屬性值,具體的邏輯體現在如下的代碼片斷中。也就是說,在默認的情況下ASP.NET MVC會采用這三種FilterProvider來提供所有的Filter對象。

   1: public static class FilterProviders
   2: { 
   3:     static FilterProviders()
   4:     {
   5:         Providers = new FilterProviderCollection();
   6:         Providers.Add(GlobalFilters.Filters);
   7:         Providers.Add(new FilterAttributeFilterProvider());
   8:         Providers.Add(new ControllerInstanceFilterProvider());
   9:     }
  10:     
  11:     public static FilterProviderCollection Providers{get;private set;}
  12: }

為了讓讀者對上麵介紹的Filter提供機製具有一個更加深刻的映像,我們來做一個簡單的實例演示。在一個通過Visual Studio的ASP.NET MVC項目模板創建的空Web項目中,我們定義了如下一個幾個FilterAttribute。FilterBaseAttribute是一個實現了IActionFilter接口的抽象類型,三個具體的FilterAttribute(FooAttribute、BarAttribute和BazAttribute)是它的繼承者。

   1: public abstract class FilterBaseAttribute:FilterAttribute, IActionFilter
   2: {
   3:     public void OnActionExecuted(ActionExecutedContext filterContext)
   4:     {}
   5:  
   6:     public void OnActionExecuting(ActionExecutingContext filterContext)
   7:     {}
   8: }
   9:  
  10: public class FooAttribute : FilterBaseAttribute
  11: {}
  12: public class BarAttribute : FilterBaseAttribute
  13: {}
  14: public class BazAttribute : FilterBaseAttribute
  15: {} 

我們首先在Global.asax中通過如下的方式將BazAttribute注冊為一個全局篩選器。需要注意的是定義在默認創建的Global.asax中的Application_Start方法會調用RegisterGlobalFilters方法注冊一個類型為HandleErrorAttribute的ExceptionFilter,我們需要將這行代碼注釋。

   1: public class MvcApplication : System.Web.HttpApplication
   2: {
   3:     //其他成員
   4:     protected void Application_Start()
   5:     {        
   6:         //其他操作
   7:         //RegisterGlobalFilters(GlobalFilters.Filters);        
   8:         GlobalFilters.Filters.Add(new BazAttribute());
   9:     }
  10: }

最後我們創建如下一個默認的HomeController,一個空的Action方法Data上應用了我們定義的BarAttribute特性,而HomeController類上則應用了FooAttribute特性。在默認的Action方法Index中,我們通過FilterProviders的靜態屬性Providers表示的全局FilterProvider列表得到針對於Action方法Data的所有Filter對象,並將它們的基本信息(類型、Order和Scope屬性)呈現出來。

   1: [Foo]
   2: public class HomeController : Controller
   3: {
   4:     public void Index()
   5:     {
   6:         ReflectedControllerDescriptor controllerDescriptor = new ReflectedControllerDescriptor(typeof(HomeController));
   7:         ActionDescriptor actionDescriptor = controllerDescriptor.FindAction(ControllerContext, "Data");
   8:         foreach (var filter in FilterProviders.Providers.GetFilters(ControllerContext, actionDescriptor))
   9:         { 
  10:             Response.Write(string.Format("{0}<br/>",filter.Instance));
  11:             Response.Write(string.Format("&nbsp;&nbsp;&nbsp;&nbsp;{0}: {1}<br/>", "Order",filter.Order));
  12:             Response.Write(string.Format("&nbsp;&nbsp;&nbsp;&nbsp;{0}: {1}<br/><br/>", "Scope",filter.Scope));
  13:         }
  14:     }
  15:     [Bar]
  16:     public void Data()
  17:     { }
  18: }

運行我們的程序之後會在瀏覽器中呈現如圖7-5所示的結果。我們可以清楚地看到,不僅僅應用在自身Action方法的FilterAttribute會應用到目標Action上,應用在Controller類的FilterAttribute、全局注冊的Filter以及Controller對象本身體現的Filter都回最終應用在所有的Action上麵。

image

上圖將應用於Action方法Data的4個Filter的Order和Scope屬性顯示出來。我們在前麵提到這兩個屬性決定了同類篩選器執行的順序,我們現在利用這個程序要證實這一點。為此我們需要對FilterBaseAttribute作如下的修改,在OnActionExecuting中我們將當前執行的FilterAttribute的類型的方法名呈現出來。

   1: public abstract class FilterBaseAttribute:FilterAttribute, IActionFilter
   2: {
   3:     public void OnActionExecuted(ActionExecutedContext filterContext)
   4:     {}
   5:     public void OnActionExecuting(ActionExecutingContext filterContext)
   6:     {
   7:         filterContext.HttpContext.Response.Write(string.Format("{0}.OnActionExecuting()<br/>", this.GetType()));
   8:     }
   9: }

然後我們按照相同的方式重寫了HomeController的OnActionExecuting方法,將HomeController自身的類型的當前方法名稱呈現出來。

   1: [Foo]
   2: public class HomeController : Controller
   3: {
   4:     //其他成員
   5:     protected override void OnActionExecuting(ActionExecutingContext filterContext)
   6:     {
   7:         Response.Write("HomeController.OnActionExecuting()<br/>");
   8:     }
   9:  
  10:     [Bar]
  11:     public void Data()
  12:     { }
  13: }

我們再次運行我們的程序,並在瀏覽器上指定正確的地址訪問定義在HomeController的Action方法Data,會在瀏覽器中呈現如下圖所示的結果。輸出的結果體現了應用到Action方法Data上的四個ActionFilter執行的順序,而這是和Filter對應的Order和Scope屬性值是一致的。

image

關於Filter的提供還另一個值得深究的問題:我們在定義FilterAttribute的時候可以將應用在該類型上的AttributeUsageAttribute的AllowMultiple屬性設置為False使它隻能在同一個目標元素上應用一次。但是,?

我們現在就來通過實例來驗證這一點。現在我們刪除所有的FilterAttribute,定義如下一個類型為FooAttribute的ActionFilter,我們將應用在它上麵的AttributeUsageAttribute特性的AllowMultiple屬性設置為False。

   1: [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, )]
   2: public class FooAttribute : FilterAttribute, IActionFilter
   3: {
   4:     public void OnActionExecuted(ActionExecutedContext filterContext)
   5:     { }
   6:     public void OnActionExecuting(ActionExecutingContext filterContext)
   7:     { }
   8: }

現在我們將該FooAttribute特性同時應用在HomeController類型和Action方法Data上,然後在Global.asax中注冊一個針對FooAttribute特性的全局Filter。

   1
   2: public class HomeController : Controller
   3: {
   4:     //其他成員
   5:     [Foo]
   6:     public void Data()
   7:     { }
   8:  
   9: }
  10:  
  11: public class MvcApplication : System.Web.HttpApplication
  12: {
  13:     //其他成員
  14:     protected void Application_Start()
  15:     {
  16:         //其他操作
  17:         //RegisterGlobalFilters(GlobalFilters.Filters);
  18:         GlobalFilters.Filters.Add(new ());
  19:     }
  20: }

現在我們直接運行我們的程序,開啟的瀏覽器中會呈現出如圖7-7所示的結果。可以清楚地看到雖然我們 在三個地方注冊了FooAttribute,但是由於該特性的AllowMultiple屬性為False,所以隻有其中一個FooAttribute最終是有效的。

image

對於AllowMultiple屬性為False的FilterAttribute來說,如果我們以不同的Scope注冊了多個,最終有效的是哪個呢?從上圖可以看出,應用在Action方法(Scope為Action)上的FooAttribute是有效的。其實具體的邏輯是這樣的:。對於我們的例子來說,提供的三個Filter具有相同的Order屬性值(-1),所有最終會按照Scope(Scope、Controller和Action)進行排序,排在最後一個的自然是Scope為Action的Filter。


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-25 17:04:28

  上一篇:go  記 php-fpm 重啟導致的 程序執行中斷問題
  下一篇:go  認識ASP.NET MVC的5種AuthorizationFilter