ASP.NET MVC Controller啟用系統詳解:預設實現

weixin_33816946發表於2012-03-31

Controller啟用系統最終通過註冊的ControllerFactory建立相應的Conroller物件,如果沒有對ControllerFactory型別或者型別進行顯式註冊(通過呼叫當前ControllerBuilder的SetControllerFactory方法),預設使用的是一個DefaultControllerFactory物件,我們現在就來討論實現在DefaultControllerFactory型別中的預設Controller啟用機制。

目錄
一、Controller型別的解析
    例項演示:建立一個自定義ControllerFactory模擬Controller預設啟用機制
二、 Controller型別的快取
三、 Controller的釋放
四、會話狀態行為的控制

一、Controller型別的解析

啟用目標Controller物件的前提是能夠正確解析出對應的Controller型別。對於DefaultControllerFactory來,用於解析目標Controller型別的資訊包括:通過與當前請求匹配的路由物件生成的RouteData(其中包含Controller的名稱和名稱空間)和包含在當前ControllerBuilder中的名稱空間。很對讀者可以首先想到的是通過Controller名稱得到對應的型別,並通過名稱空間組成Controller型別的全名,最後遍歷所有程式集以此名稱去載入相應的型別即可。

這貌似一個不錯的解決方案,實際上則完全不可行。不要忘了作為請求地址URL一部分的Controller名稱是不區分大小寫的,而型別名稱則是區分大小的;不論是註冊路由時指定的名稱空間還是當前ControllerBuilder的預設名稱空間,有可能是包含統配符(*)。由於我們不能通過給定的Controller名稱和名稱空間得到Controller的真實型別名稱,自然就不可能通過名稱去解析Controller的型別了。

ASP.NET MVC的Controller啟用系統反其道而行之。它先遍歷通過BuildManager的靜態方法GetReferencedAssemblies方法得到的編譯Web應用所使用的程式集,通過反射得到所有實現了介面IController的型別,最後通過給定的Controller的名稱和名稱空間作為匹配條件在這個預先獲取的型別列表中得到目標Controller的型別

例項演示:建立一個自定義ControllerFactory模擬Controller預設啟用機制

為了讓讀者對預設採用的Controller啟用機制,尤其是Controller型別的解析機制有一個深刻的認識,我們通過一個自定義的ControllerFactory來模擬其中的實現。由於我們採用反射的方式來建立Controller物件,所以我們將該自定義ControllerFactory起名為ReflelctionControllerFactory。[原始碼從這裡下載]

   1: public class ReflelctionControllerFactory : IControllerFactory
   2: {
   3:     //其他成員
   4:     private static List<Type> controllerTypes;
   5:     static ReflelctionControllerFactory()
   6:     {
   7:         controllerTypes = new List<Type>();
   8:         foreach (Assembly assembly in BuildManager.GetReferencedAssemblies())
   9:         {
  10:             controllerTypes.AddRange(assembly.GetTypes().Where(type => typeof(IController).IsAssignableFrom(type)));
  11:         }
  12:     }
  13:  
  14:     public IController CreateController(RequestContext requestContext, string controllerName)
  15:     {
  16:         Type controllerType = this.GetControllerType(requestContext.RouteData, controllerName);
  17:         if (null == controllerType)
  18:         {
  19:             throw new HttpException(404, "No controller found");
  20:         }
  21:         return (IController)Activator.CreateInstance(controllerType);
  22:     }
  23:  
  24:     private static bool IsNamespaceMatch(string requestedNamespace, string targetNamespace)
  25:     {
  26:         if (!requestedNamespace.EndsWith(".*", StringComparison.OrdinalIgnoreCase))
  27:         {
  28:             return string.Equals(requestedNamespace, targetNamespace, StringComparison.OrdinalIgnoreCase);
  29:         }
  30:         requestedNamespace = requestedNamespace.Substring(0, requestedNamespace.Length - ".*".Length);
  31:         if (!targetNamespace.StartsWith(requestedNamespace, StringComparison.OrdinalIgnoreCase))
  32:         {
  33:             return false;
  34:         }
  35:         return ((requestedNamespace.Length == targetNamespace.Length) || (targetNamespace[requestedNamespace.Length] == '.'));
  36:     }
  37:  
  38:    private Type GetControllerType(IEnumerable<string> namespaces, Type[] controllerTypes)
  39:     {
  40:         var types = (from type in controllerTypes
  41:                         where namespaces.Any(ns => IsNamespaceMatch(ns, type.Namespace))
  42:                         select type).ToArray();
  43:         switch (types.Length)
  44:         {
  45:             case 0: return null;
  46:             case 1: return types[0];
  47:             default: throw new InvalidOperationException("Multiple types were found that match the requested controller name.");
  48:         }
  49:     }
  50:  
  51:     protected virtual Type GetControllerType(RouteData routeData, string controllerName)
  52:     {
  53:         //省略實現
  54:     }
  55: }

如上面的程式碼片斷所示,ReflelctionControllerFactory具有一個靜態的controllerTypes欄位由於儲存所有Controller的型別。在靜態建構函式中,我們呼叫BuildManager的GetReferencedAssemblies方法得到所有用於編譯Web應用的程式集,並從中得到所有實現了IController介面的型別,這些型別全部被新增到通過靜態欄位controllerTypes表示的型別列表。

Controller型別的解析實現在受保護的GetControllerType方法中,在用於最終啟用Controller物件的CreateController方法中,我們通過呼叫該方法得到與指定RequestContext和Controller名稱相匹配的Controller型別,最終通過呼叫Activator的靜態方法CreateInstance根據該型別建立相應的Controller物件。如果不能找到匹配的Controller型別(GetControllerType方法返回Null),則丟擲一個HTTP狀態為404的HttpException。

ReflelctionControllerFactory中定義了兩個輔助方法,IsNamespaceMatch用於判斷Controller型別真正的名稱空間是否與指定的名稱空間(可能包含統配符)相匹配,在進行字元比較過程中是忽略大小寫的。私有方法GetControllerType根據指定的名稱空間列表和型別名稱匹配的型別陣列得到一個完全匹配的Controller型別。如果得到多個匹配的型別,直接丟擲InvalidOperation異常,並提示具有多個匹配的Controller型別;如果找不到匹配型別,則返回Null。

在如下所示的用於解析Controller型別的GetControllerType方法中,我們從預先得到的所有Controller型別列表中篩選出型別名稱與傳入的Controller名稱相匹配的型別。我們首先通過路由物件的名稱空間對 之前 得到的型別列表進行進一步篩選,如果能夠找到一個唯一的型別,則直接將其作為Controller的型別返回。為了確定是否採用後備名稱空間對Controller型別進行解析,我們從作為引數引數的RouteData物件的DataTokens中得到獲取一個Key為“UseNamespaceFallback”的元素,如果該元素存在並且值為False,則直接返回Null。

如果RouteData的DataTokens中不存在這樣一個UseNamespaceFallback元素,或者它的值為True,則首先裡當前ControllerBuilder的預設名稱空間列表進一步對Controller型別進行解析,如果存在唯一的型別則直接當作目標Controller型別返回。如果通過兩組名稱空間均不能得到一個匹配的ControllerType,並且只存在唯一一個與傳入的Controller名稱相匹配的型別,則直接將該型別作為目標Controller返回。如果這樣的型別具有多個,則直接丟擲InvalidOperationException異常。

   1: public class ReflelctionControllerFactory : IControllerFactory
   2: {
   3:     //其他成員
   4:     protected virtual Type GetControllerType (RouteData routeData, string controllerName)
   5:     {
   6:         //根據型別名稱篩選
   7:         var types = controllerTypes.Where(type => string.Compare(controllerName + "Controller", type.Name, true) == 0).ToArray();
   8:         if (types.Length == 0)
   9:         {
  10:             return null;
  11:         }
  12:  
  13:         //通過路由物件的名稱空間進行匹配
  14:         var namespaces = routeData.DataTokens["Namespaces"] as IEnumerable<string>;
  15:         namespaces = namespaces ?? new string[0];
  16:         Type contrllerType = this.GetControllerType(namespaces, types);
  17:         if (null != contrllerType)
  18:         {
  19:             return contrllerType;
  20:         }
  21:  
  22:         //是否允許採用後備名稱空間
  23:         bool useNamespaceFallback = true;
  24:         if (null != routeData.DataTokens["UseNamespaceFallback"])
  25:         {
  26:             useNamespaceFallback = (bool)(routeData.DataTokens["UseNamespaceFallback"]);
  27:         }
  28:  
  29:         //如果不允許採用後備名稱空間,返回Null
  30:         if (!useNamespaceFallback)
  31:         {
  32:             return null;
  33:         }
  34:  
  35:         //通過當前ControllerBuilder的預設名稱空間進行匹配
  36:         contrllerType = this.GetControllerType(ControllerBuilder.Current.DefaultNamespaces, types);
  37:         if (null != contrllerType)
  38:         {
  39:             return contrllerType;
  40:         }
  41:  
  42:         //如果只存在一個型別名稱匹配的Controller,則返回之
  43:         if (types.Length == 1)
  44:         {
  45:             return types[0];
  46:         }
  47:  
  48:         //如果具有多個型別名稱匹配的Controller,則丟擲異常
  49:         throw new InvalidOperationException("Multiple types were found that match the requested controller name.");
  50:     }
  51: }

二、 Controller型別的快取

為了避免通過遍歷所有程式集對目標Controller型別的解析,ASP.NET MVC對解析出來的Controller型別進行了快取以提升效能。與針對用於Area註冊的AreaRegistration型別的快取類似,Controller啟用系統同樣採用基於檔案的快取策略,而用於儲存Controller型別列表的名為MVC-ControllerTypeCache.xml的檔案儲存在ASP.NET的臨時目錄下面。具體的路徑如下,其中第一個針對寄宿於IIS中的Web應用,後者針對直接通過Visual Studio Developer Server作為宿主的應用。而用於儲存所有AreaRegistration型別列表的MVC-AreaRegistrationTypeCache.xml檔案也儲存在這個目錄下面。

  • %Windir%\Microsoft.NET\Framework\v{version}\Temporary ASP.NET Files\{appname}\...\...\UserCache\
  • %Windir%\Microsoft.NET\Framework\v{version}\Temporary ASP.NET Files\root\...\...\UserCache\

對針對Web應用被啟動後的第一個請求時,Controller啟用系統會讀取這個用於快取所有Controller型別列表的ControllerTypeCache.xml檔案並反序列化成一個List<Type>物件。只有在該列表為空的時候才會通過遍歷程式集和反射的方式得到所有實現了介面IController的公有型別,而被解析出來的Controller型別重寫被寫入ControllerTypeCache.xml檔案中。這個通過讀取快取檔案或者重新解析出來的Controller型別列表被儲存到內容中,在Web應用活動期間內被Controller啟用系統所用。

下面的XML片斷反映了這個用於Controller型別列表快取的ControllerTypeCache.xml檔案的結構,我們可以看出它包含了所有的Controller型別的全名和所在的程式集和託管模組資訊。

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <!--This file is automatically generated. Please do not modify the contents of this file.-->
   3: <typeCache lastModified="3/22/2012 1:18:49 PM" mvcVersionId="80365b23-7a1d-42b2-9e7d-cc6f5694c6d1">
   4:   <assembly name="Artech.Admin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
   5:     <module versionId="eb343e3f-2d63-4665-a12a-29fb30dceeed">
   6:       <type> Artech.Admin .HomeController</type>
   7:       <type> Artech.Admin .EmployeeController </type>
   8:     </module>
   9:   </assembly>
  10:   <assembly name="Artech.Portal, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null">
  11:     <module versionId=" 3717F116-35EE-425F-A1AE-EB4267497D8C ">
  12:       <type>Artech. Portal.Controllers.HomeController</type>
  13:       <type>Artech. Portal.ProductsController</type>
  14:     </module>
  15:   </assembly>
  16: </typeCache>

三、 Controller的釋放

作為啟用Controller物件的ControllerFactory不僅僅用於建立目標Controller物件,還具有兩個額外的功能,即通過ReleaseController方法對啟用的Controller物件進行釋放和回收,以及通過GetControllerSessionBehavior返回用於控制當前會話狀態行為的SessionStateBehavior列舉。

對於預設使用DefaultControllerFactory來說,針對Controller物件的釋放操作很簡單:如果Controller型別實現了IDisposable介面,則直接呼叫其Dispose方法即可;否則直接忽略。我們將這個邏輯也實現在了我們自定義的ReflelctionControllerFactory中。

   1: public class ReflelctionControllerFactory : IControllerFactory
   2: {
   3:     //其他操作
   4:     public void ReleaseController(IController controller)
   5:     {
   6:         IDisposable disposable = controller as IDisposable;
   7:         if (null != disposable)
   8:         {
   9:             disposable.Dispose();
  10:         }
  11:     }
  12: }

四、會話狀態行為的控制

至於用於返回SessionStateBehavior列舉的GetControllerSessionBehavior方法來說,在預設的情況下的返回值為SessionStateBehavior.Default。通過前面的介紹我們知道在這種情況下具體的會話狀態行為取決於建立的HttpHandler所實現的標記介面。對於ASP.NET MVC應用來說,預設用於處理請求的HttpHandler是一個叫做MvcHandler的物件,如下面的程式碼片斷所示,HttpHandler實現了IRequiresSessionState介面,意味著預設情況下會話狀態是可讀寫的(相當於SessionStateBehavior.Requried)。

   1: public class MvcHandler : 
   2: IHttpAsyncHandler, 
   3: IHttpHandler, 
   4: IRequiresSessionState
   5: {
   6:     //其他成員
   7: }

不過我們可以通過在Controller型別上應用SessionStateAttribute特性來具體控制會話狀態行為。如下面的程式碼片斷所示,SessionStateAttribute具有一個SessionStateBehavior型別的只讀屬性Behavior用於返回具體行為設定的會話狀態行為選項,該屬性是在建構函式中被初始化的。

   1: [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
   2: public sealed class SessionStateAttribute : Attribute
   3: {
   4:     public SessionStateAttribute(SessionStateBehavior behavior);
   5:     public SessionStateBehavior Behavior { get; }
   6: }

也就是說DefaultControllerFactory會通過解析出來的Controller型別得到應用在上面的SessionStateAttribute特性,如果這樣的特性存在則直接返回它的Behavior屬性所表示的SessionStateBehavior列舉;如果不存在則返回SessionStateBehavior.Default,具體的邏輯反映在我們自定義的ReflelctionControllerFactory的GetControllerSessionBehavior方法中。

   1: public class ReflelctionControllerFactory : IControllerFactory
   2: {
   3:     //其他成員
   4:     public SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName)
   5:     {
   6:         Type controllerType = this.GetControllerType(requestContext.RouteData, controllerName);
   7:         if (null == controllerType)
   8:         {
   9:             return SessionStateBehavior.Default;
  10:         }
  11:         SessionStateAttribute attribute = controllerType.GetCustomAttributes(true).OfType<SessionStateAttribute>()
  12:            .FirstOrDefault();
  13:         attribute = attribute ?? new SessionStateAttribute(SessionStateBehavior.Default);
  14:         return attribute.Behavior;
  15:     }    
  16: }
ASP.NET MVC Controller啟用系統詳解:總體設計
ASP.NET MVC Controller啟用系統詳解:預設實現
ASP.NET MVC Controller啟用系統詳解:IoC的應用[上篇]
ASP.NET MVC Controller啟用系統詳解:IoC的應用[下篇]

相關文章