您好,登錄后才能下訂單哦!
本篇內容主要講解“ASP.NET MVC Controller激活系統怎么實現”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“ASP.NET MVC Controller激活系統怎么實現”吧!
一、Controller
我們知道作為Controller的類型直接或者間接實現了IController接口。如下面的代碼片斷所示,IController接口僅僅包含一個參數類型為RequestContext的Execute方法。當一個Controller對象被激活之后,核心的操作就是根據請求上下文解析出目標Action方法,并通過Model綁定機制從請求上下文中提取相應的數據映射為方法的參數并最終執行Action方法。所有的這些操作都是調用這個Execute方法來執行的。
public interface IController { void Execute(RequestContext requestContext);
定義在IController接口中的Execute是以同步的方式執行的。為了支持以異步方式對請求的處理,IController接口的異步版本System.Web.Mvc.IAsyncController被定義出來。如下面的代碼片斷所示,實現了IAsyncController接口的異步Controller的執行通過BeginExecute/EndExecute方法組合來完成。
public interface IAsyncController : IController { IAsyncResult BeginExecute(RequestContext requestContext, AsyncCallback callback, object state); void EndExecute(IAsyncResult asyncResult); }
抽象類ControllerBase實現了IController接口,它具有如下幾個重要的屬性。TemplateData、ViewBag和ViewData用于存儲從Controller向View傳遞的數據或者變量。其中TemplateData和ViewData具有基于字典的數據結構,Key和Value分別表示變量的名稱和值,所不同的前者用于存儲基于當前HTTP上下文的變量(在完成當前請求后,存儲的數據會被回收)。ViewBag和ViewData具有相同的作用,甚至對應著相同的數據存儲,它們之間的不同之處在于前者是一個動態對象,我們可以為其指定任意屬性。
public abstract class ControllerBase : IController { //其他成員 public ControllerContext ControllerContext { get; set; } public TempDataDictionary TempData { get; set; } public object ViewBag { [return: Dynamic] get; } public ViewDataDictionary ViewData { get; set; }
在ASP.NET MVC中我們會陸續遇到一系列的上下文(Context)對象,之前我們已經對表示請求上下文的RequestContext(HttpContext + RouteData)進行了詳細的介紹,現在我們來介紹另一個具有如下定義的上下文類型ControllerContext。
public class ControllerContext { //其他成員 public ControllerContext(); public ControllerContext(RequestContext requestContext, ControllerBase controller); public ControllerContext(HttpContextBase httpContext, RouteData routeData, ControllerBase controller); public virtual ControllerBase Controller { get; set; } public RequestContext RequestContext { get; set; } public virtual HttpContextBase HttpContext { get; set; } public virtual RouteData RouteData { get; set; } }
顧名思義,ControllerContext就是基于某個Controller對象的上下文。從如下的代碼所示,ControllerContext是實際上是對一個Controller對象和RequestContext的封裝,這兩個對象分別對應著定義在ControllerContext中的同名屬性,并且可以在構造函數中被初始化。而通過屬性HttpContext和RouteData屬性返回的HttpContextBase和RouteData對象在默認情況下實際上就是組成RequestContext的核心元素。ControllerContext的這四個屬性都是可讀可寫的,我們對其進行任意地修改。當ControllerBase的Execute方法被執行的時候,它會根據傳入的ReuqestContext創建ControllerContext對象,而后續的操作可以看成是在該上下文中進行。
當我們在進行開發的時候,通過VS默認創建的Controller類型實際上繼承自抽象類Controller。該類型中定義了很多的輔助方法和屬性以編程變得簡單。如下面的代碼片斷所示,除了直接繼承ControllerBase之外,Controller類型還顯式實現了IController和IAsyncController接口,以及代表ASP.NET MVC 四大篩選器(AuthorizationFilter、ActionFilter、ResultFilter和ExceptionFilter)的4個接口。
public abstract class Controller : ControllerBase, IController, IAsyncController, IActionFilter, IAuthorizationFilter, IExceptionFilter, IResultFilter, IDisposable, ... { //省略成員 }
二、 ControllerFactory
ASP.NET MVC為Controller的激活定義相應的相應的工廠,我們將其統稱為ControllerFactory,所有的ControllerFactory實現了接口IControllerFactory接口。如下面的代碼片斷所示,Controller對象的激活最終最終通過IControllerFactory的CreateController方法來完成,該方法的兩個參數分別表示當前請求上下文和從路由信息中獲取的Controller的名稱(最初來源于請求地址)。
public interface IControllerFactory { IController CreateController(RequestContext requestContext, string controllerName); SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName); void ReleaseController(IController controller); } public enum SessionStateBehavior Default, Required, ReadOnly, Disabled }
處理負責創建Controller處理請求之前,ControllerFactory還需要在完成請求處理之后實施對Controller的釋放回收,后者實現在ReleaseController方法中。IControllerFactory的另一個方法GetControllerSessionBehavior方法返回一個SessionStateBehavior枚舉。熟悉ASP.NET的讀者應該對SessionStateBehavior不會感到陌生,它用于表示請求處理過程中會話狀態支持的模式,它的四個枚舉值分別具有如下的含義:
Default:使用默認 ASP.NET 邏輯來確定請求的會話狀態行為。
Required:為請求啟用完全的讀寫會話狀態行為。
ReadOnly:為請求啟用只讀會話狀態。
Disabled:禁用會話狀態。
對于Default選項來說,ASP.NET通過映射的HttpHandler類型是否實現了相關接口來決定具體的會話狀態控制行為。在System.Web.SessionState命名空間下定義了IRequiresSessionState和IRequiresSessionState接口,如下面的代碼片斷所示,這兩個都是不具有任何成員的空接口(我們一般稱之為標記接口),而IReadOnlySessionState繼承自IRequiresSessionState。如果HttpHandler實現了接口IReadOnlySessionState,則意味著采用ReadOnly模式,如果只實現了IRequiresSessionState則采用Required模式。
1: public interface IRequiresSessionState
2: {}
3: public interface IReadOnlySessionState : IRequiresSessionState
4: {}
具體采用何種會話狀態行為取決于當前HTTP上下文(HttpContext.Current)。對于之前的版本,我們不能對當前HTTP上下文的會話狀態行為模式進行動態的修改,ASP.NET 4.0為HttpContext定義了如下一個SetSessionStateBehavior方法是我們可以自由地選擇會話狀態行為模式。相同的方法同樣定義在HttpContextBase中,它的子類HttpContextWrapper重寫了這個方法并在內部會調用封裝的HttpContext的同名方法。
public sealed class HttpContext : IServiceProvider, IPrincipalContainer { //其他成員 public void SetSessionStateBehavior( SessionStateBehavior sessionStateBehavior); } public class HttpContextBase: IServiceProvider { //其他成員 public void SetSessionStateBehavior(SessionStateBehavior sessionStateBehavior); }
三、ControllerBuilder
用于激活Controller對象的ControllerFactory最終通過ControllerBuilder注冊到ASP.NET MVC應用中。如下面的代碼所示,ControllerBuilder定義了一個靜態只讀屬性Current返回當前ControllerBuilder對象,這是針對整個Web應用的全局對象。兩個SetControllerFactory方法重載用于注冊ControllerFactory的類型或者實例,而GetControllerFactory方法返回一個具體的ControllerFactory對象。
public class ControllerBuilder public IControllerFactory GetControllerFactory(); public void SetControllerFactory(Type controllerFactoryType); public void SetControllerFactory(IControllerFactory controllerFactory); public HashSet<string> DefaultNamespaces { get; } public static ControllerBuilder Current { get; } }
具體來說,如果我們是注冊的ControllerFactory的類型,那么GetControllerFactory在執行的時候會通過對注冊類型的反射(調用Activator的靜態方法CreateInstance)來創建具體的ControllerFactory(系統不會對創建的Controller進行緩存);如果注冊的是一個具體的ControllerFactory對象,該對象直接從GetControllerFactory返回。
被ASP.NET路由系統進行攔截處理后會生成一個用于封裝路由信息的RouteData對象,而目標Controller的名稱就包含在通過該RouteData的Values屬性表示的RouteValueDisctionary對象中,對應的Key為“controller”。而在默認的情況下,這個作為路由數據的名稱只能幫助我們解析出Controller的類型名稱,如果我們在不同的命名空間下定義了多個同名的Controller類,會導致激活系統無法確定具體的Controller的類型從而拋出異常。
為了解決這個問題,我們必須為定義了同名Controller類型的命名空間設置不同的優先級,具體來說我們有兩種提升命名空間優先級的方式。***種方式就是在調用RouteCollection的擴展方法MapRoute時指定一個命名空間的列表。通過這種方式指定的命名空間列表會保存在Route對象的DataTokens屬性表示的RouteValueDictionary字典中,對應的Key為“Namespaces”。
public static class RouteCollectionExtensions { //其他成員 public static Route MapRoute(this RouteCollection routes, string name, string url, string[] namespaces); public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces); public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces); }
而另一種提升命名空間優先級的方式就是將其添加到當前的ControllerBuilder中的默認命名空間列表中。從上面的給出的ControllerBuilder的定義可以看出,它具有一個HashSet<string>類型的只讀屬性DefaultNamespaces就代表了這么一個默認命名空間列表。對于這兩種不同的命名空間優先級提升方式,前者(通過路由注冊)指定命名空間具有更高的優先級。
實例演示:如何提升命名空間的優先級
為了讓讀者對此如何提升命名空間優先級具有一個深刻的印象,我們來進行一個簡單的實例演示。我們使用Visual Studio提供的項目模板創建一個空的ASP.NET MVC應用,并且使用如下所示的默認路由注冊代碼。
public class MvcApplication : System.Web.HttpApplication { public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } protected void Application_Start() { //其他操作 RegisterRoutes(RouteTable.Routes); } } public class MvcApplication : System.Web.HttpApplication { public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } ); } protected void Application_Start() { //其他操作 RegisterRoutes(RouteTable.Routes); } }
然后我們在Controllers目錄下添加一個.cs 文件,并在該文件中定義兩個同名的Controller類。如下面的代碼片斷所示,這兩個HomeCotroller類分別定義在命名空間Artech.MvcApp和Artech.MvcApp.Controllers之中,而Index操作返回的是一個將Controller類型全名為內容的ContentResult對象。
namespace Artech.MvcApp.Controllers { public class HomeController : Controller { public ActionResult Index() { return this.Content(this.GetType().FullName); } } namespace Artech.MvcApp { public class HomeController : Controller { public ActionResult Index() { return this.Content(this.GetType().FullName); } }
現在我們直接運行該Web應用。由于具有多個Controller與注冊的路由規則相匹配導致ASP.NET MVC的Controller激活系統無法確定目標哪個類型的Controller應該被選用,所以會出現如下圖所示的錯誤。
目前定義了HomeController的兩個命名空間具有相同的優先級,現在我們將其中一個定義在當前ControllerBuilder的默認命名空間列表中以提升匹配優先級。如下面的代碼片斷所示,在Global.asax 的Application_Start方法中,我們將命名空間“Artech.MvcApp.Controllers”添加到當前ControllerBuilder的DefaultNamespaces屬性所示的命名空間列表中。
1: public class MvcApplication : System.Web.HttpApplication
2: {
3: protected void Application_Start()
4: {
5: //其他操作
6: ControllerBuilder.Current.DefaultNamespaces.Add("Artech.MvcApp.Controllers");
7: }
8: }
對用同時匹配注冊的路由規則的兩個HomeController,由于“Artech.MvcApp.Controllers”命名空間具有更高的匹配優先級,所有定義其中的HomeController會被選用,這可以通過如下圖所示的運行結果看出來。
為了檢驗在路由注冊時指定的命名空間和作為當前ControllerBuilder的命名空間哪個具有更高匹配優先級,我們修改定義在Global.asax中的路由注冊代碼。如下面的代碼片斷所示,我們在調用RouteTable的靜態屬性Routes的MapRoute方法進行路由注冊的時候指定了命名空間(“Artech.MvcApp”)。
public class MvcApplication : System.Web.HttpApplication { public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, namespaces:new string[]{"Artech.MvcApp"} ); } protected void Application_Start() { //其他操作 RegisterRoutes(RouteTable.Routes); ControllerBuilder.Current.DefaultNamespaces.Add("Artech.MvcApp.Controllers"); }
再次運行我們的程序會在瀏覽器中得到如圖3-3所示的結果,從中可以看出定義在命名空間“Artech.MvcApp”中的HomeController被最終選用,可見較之作為當前ControllerBuilder的默認命名空間,在路由注冊過程中執行的命名空間具有更高的匹配優先級,前者可以視為后者的一種后備。
在路由注冊時指定的命名空間比當前ControllerBuilder的默認命名空間具有更高的匹配優先級,但是對于這兩個集合中的所有命名空間卻具有相同的匹配優先級。換句話說,用于輔助解析Controller類新的命名空間分為三個梯隊,簡稱為路由命名空間、ConrollerBuilder命名空間和Controller類型命名空間,如果前一個梯隊不能正確解析出目標Controller的類型,則將后一個梯隊的命名空間作為后備;反之,如果根據某個梯隊的命名空間進行解析得到多個匹配的Controller類型,會直接拋出異常。
現在我們對本例的路由注冊代碼作了如下的修改,為注冊的路由對象指定了兩個命名空間(分別是兩個HomeContrller所在的命名空間),運行我們的程序依然會得到如***張圖所示的錯誤。
public class MvcApplication : System.Web.HttpApplication { public static void RegisterRoutes(RouteCollection routes) { routes.MapRoute( name: "Default", url: "{controller}/{action}/{id}", defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, namespaces: new string[] { "Artech.MvcApp", "Artech.MvcApp.Controllers" } ); } protected void Application_Start() { //其他操作 RegisterRoutes(RouteTable.Routes); } }
針對Area的路由對象的命名空間
針對某個Area的路由映射是通過相應的AreaRegistration進行注冊的,具體來說是在AreaRegistration的RegisterArea方法中調用AreaRegistrationContext對象的MapRoute方法進行注冊的。如果在調用MapRoute方法中指定了表示命名空間的字符串,將自動作為注冊的路由對象的命名空間,否則會將表示AreaRegistration所在命名空間的字符串加上“.*”后綴作為路由對象的命名空間。這里所說的“路由對象的命名空間”指的就是通過Route對象的DataTokens屬性表示的RouteValueDictionary對象中Key為“Namespaces”的字符串數組,而該字符串最終會轉移到生成的RouteData的DataTokens中。
除此之外,在調用AreaRegistrationContext的MapRoute方法時還會在注冊Route對象DataTokens中添加一個Key為“UseNamespaceFallback”的條目表示是否采用后備命名空間對Controller類型進行解析。如果注冊對象具有命名空間(調用MapRoute方法時指定了命名空間或者對應的AreaRegistration類型定義在某個命名空間中),該條目的值為False;否則為True。該條目同樣反映在通過該Route對象生成的RouteData對象的DataTokens屬性中。[關于ASP.NET MVC路由,在我的文章《ASP.NET MVC路由擴展:路由映射》中具有詳細的介紹]
在解析Controller真實類型的過程中,會先通過RouteData包含的命名空間來解析Controller類型。如果Controller類型解析失敗,則通過包含在通過RouteData的DataTokens屬性表示的RouteValueDictionary對象中的這個UseNamespaceFallback值來判斷是否使用“后備”命名空間進行解析。具體來說,如果該值為True或者不存在,則先通過當前ControllerBuilder的命名空間解析,如果失敗則忽略命名空間直接采用類型名稱進行匹配;否則直接因找不到匹配的Controller而拋出異常。
我們通過具體的例子來說明這個問題。在一個通過Visual Studio的ASP.NET MVC項目創建的空Web應用中,我們添加一個名稱為Admin的Area,此時IDE會默認為我們添加如下一個AdminAreaRegistration類型。
1: namespace Artech.MvcApp.Areas.Admin
2: {
3: public class AdminAreaRegistration : AreaRegistration
4: {
5: public override string AreaName
6: {
7: get{return "Admin";}
8: }
9: public override void RegisterArea(AreaRegistrationContext context)
10: {
11: context.MapRoute("Admin_default", "Admin/{controller}/{action}/{id}",
12: new { action = "Index", id = UrlParameter.Optional }
13: );
14: }
15: }
16: }
AdminAreaRegistration類型定義在命名空間Artech.MvcApp.Areas.Admin中。現在我們在該Area中添加一個Controller類,其名為HomeController。默認情況下,我們添加的Controller類型和AdminAreaRegistration具有相同的命名空間,但是現在我們刻意將命名空間改為Artech.MvcApp.Areas。
namespace Artech.MvcApp.Areas { public class HomeController : Controller { public ActionResult Index() { return Content("..."); } }
現在我們在瀏覽器中通過匹配的URL(/Admin/Home/Index)來訪問Area為Admin的HomeController的Index操作,會得到如下圖所示的HTTP狀態為404的錯誤。這就是因為在對Controller類型進行解析的時候是嚴格按照對應的AreaRegistration所在命名空間來進行的,很顯然在這個范圍內是不可能找得到對應的Controller類型的。
ASP.NET路由系統是HTTP請求抵達服務端的***道屏障,它根據注冊的路由規則對攔截的請求進行匹配并解析包含目標Controller和Action名稱的路由信息。而當前ControllerBuilder具有用于激活Controller對象的ControllerFactory,我們現在看看兩者是如何結合起來的。
通過《ASP.NET路由系統實現原理:HttpHandler的動態映射》介紹我們知道ASP.NET路由系統的核心是一個叫做UrlRoutingModule的自定義HttpModule,路由的實現是它通過注冊代表當前Web應用的HttpApplication的PostResolveRequestCache事件對HttpHandler的動態映射來實現的。具體來說,它通過以RouteTable的靜態屬性Routes代表的全局路由表對請求進行匹配并得到一個RouteData對象。RouteData具有一個實現了接口IRouteHandler的屬性RouteHandler,通過該屬性的GetHttpHandler方法得到最終被映射到當前請求的HttpHandler。
對于ASP.NET MVC應用來說,RouteData的RouteHandler屬性類型為MvcRouteHandler,體現在MvcRouteHandler類型上關于HttpHandler的提供機制基本上(不是完全等同)可以通過如下的代碼來表示。MvcRouteHandler維護著一個ControllerFactory對象,該對象可以在構造函數中指定,如果沒有顯示指定則直接通過調用當前ControllerBuilder的GetControllerFactory方法獲取。
public class MvcRouteHandler : IRouteHandler { private IControllerFactory _controllerFactory; public MvcRouteHandler(): this(ControllerBuilder.Current.GetControllerFactory()) { } public MvcRouteHandler(IControllerFactory controllerFactory) { _controllerFactory = controllerFactory; } IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext) { string controllerName = (string)requestContext.RouteData.GetRequiredString("controller"); SessionStateBehavior sessionStateBehavior = _controllerFactory.GetControllerSessionBehavior(requestContext, controllerName); requestContext.HttpContext.SetSessionStateBehavior(sessionStateBehavior); return new MvcHandler(requestContext); } }
在用于提供HttpHandler的GetHttpHandler方法中,除了返回一個實現了IHttpHandler接口的MvcHandler對象之外,還需要對當前HTTP上下文的會話狀態行為模式進行設置。具體來說,首先通過包含在傳入RequestContext的RouteData對象得到Controller的名稱,該名稱連同RequestContext對象一起傳入ControllerFactory的GetControllerSessionBehavior方法得到一個類型為SessionStateBehavior的枚舉。***通過RequestContext得到表示當前HTTP上下文的HttpContextBase對象(實際上是一個HttpContextWrapper對象)并調用其SetSessionStateBehavior方法。
紹我們知道RouteData中的RouteHandler屬性最初來源于對應的Route對象的同名屬性,而當我們調用RouteCollection的擴展方法MapRoute方法時,其內部會直接創建并添加一個Route對象。由于在創建Route對象是并沒有顯式指定ControllerFactory,所以通過當前ControllerBuilder的GetControllerFactory方法得到的ControllerFactory默認被使用。
通過當前ControllerBuilder的GetControllerFactory方法得到的ControllerFactory僅僅用于獲取會話狀態行為模式,而MvcHandler真正將它用于創建Controller。MvcHandler中關于對請求處理的邏輯基本上可以通過如下的代碼片斷來體現。如下面的代碼片斷所示,MvcHandler具有一個表示當前請求上下文的RequestContext屬性,該屬性在構造函數中被初始化。
public class MvcHandler : IHttpHandler { public RequestContext RequestContext { get; private set; } public bool IsReusable { get { return false; } } public MvcHandler(RequestContext requestContext) { this.RequestContext = requestContext; } public void ProcessRequest(HttpContext context) { IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory(); string controllerName = this.RequestContext.RouteData.GetRequiredString("controller"); IController controller = controllerFactory.CreateController(this.RequestContext, controllerName); try { controller.Execute(this.RequestContext); } finally { controllerFactory.ReleaseController(controller); } } : }
在ProcessRequest方法中,通過RequestContext對象得到目標Controller的名稱,并通過它利用當前ControllerBuilder創建的ControllerFactory激活Controller對象。在執行了被激活Controller對象的Execute方法之后調用ControllerFactory的ReleaseController對其進行釋放清理工作。
到此,相信大家對“ASP.NET MVC Controller激活系統怎么實現”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。