ASP.NET MVC Controller激活系统详解1
一.引言
好久没有写博客了,前一段时间学习了Controller激活的一篇很好的博文(链接),在此做个学习总结。
二.Controller
2.1 IController
Controller类型直接或间接实现了IController接口。当一个Controller对象被激活之后,核心的操作就是根据请求上下文解析出目标Action方法,并通过Model绑定机制从请求上下文中提取相应的数据映射为方法的参数并最终执行Action方法。所有的这些操作都是调用这个Execute方法来执行的。
1 public interface IController 2 { 3 void Execute(RequestContext requestContext); 4 }
2.2 ControllerBase
抽象类ControllerBase实现了IController接口。
1 public abstract class ControllerBase:IController 2 { 3 public ControllerContext ControllerContext {get;set;} 4 public TempDataDictionary {get;set;} 5 public object ValueBag {get;set;} 6 public ViewDataDictionary {get;set;} 7 }
从上面代码可以看出ControllerBase具备以下几个属性:ControllerContext,TempDataDictionary,ValueBag,ViewDataDictionary。
TempDataDictionary,ValueBag,ViewDataDictionary用于存储从Controller向View传递的数据或变量。
2.3 ControllerContext
在MVC中我们遇到了一系列的上下文(Context)对象,在我的其他博文中对RequestContext进行了介绍,RequestContext包含HttpContext和RouteData两个属性。
1 public class ControllerContext 2 { 3 public ControllerContext(){} 4 public ControllerContext(RequestContext requestContext,ControllerBase controllerBase); 5 public ControllerContext(HttpContextBase httpContext,RouteData routeData,ControllerBase controllerBase); 6 7 public virtual ControllerBase Controller {get;set;} 8 public RequestContext RequestContext {get;set;} 9 public virtual HttpContextBase HttpContext {get;set;} 10 public virtual RouteData {get;set;} 11 12
ControllerContext就是基于某个Controller对象的上下文,ControllerContext是实际是对一个Controller对象和RequestContext对象的封装,这两个对象对象分别对应着定义在ControllerContext的同名属性,并且可以在构造函数中初始化,并且可以在构造函数中被初始化。通过属性HttpContext和RouteData属性返回的HttpContextBase和RouteData对象在默认情况下实际就是组成RequestContext的核心元素。这四个属性都是可读可写,当ControllerBase的Execute方法被执行的手,它会根据传入的RequestContext创建ControllerContext对象。
2.4 ControllerFactory
MVC为Controller的激活定义了相应的工厂,我们将其称为ControllerFactory,所有的ControllerFactory都实现了IControllerFactory接口。Controller对象的激活都是通过IControllerFactory的CreateController方法来完成的。
1 public interface IControllerFactory 2 { 3 IController CreateController(RequestContext context,string controllerName); 4 SessionStateBehavior GetControllerSessionBehavior(RequestContext request,string controllerName); 5 void ReleaseController(IController controller); 6 }
View Code
CreateFactory方法来完成Controller对象的激活,该方法的两个参数分别表示当前请求上下文和从路由信息中获取的Controller的名称(最初来源于请求地址)。
处理负责创建Controller请求之前,ControllerFactory还需要在完成请求处理之后对Controller的释放回收,回收的处理是在ReleaseController方法中。枚举SessionStateBehavior有四个项:Default,Required,Readonly,Disabled四个项。分别解释为:
Default:使用默认ASP.NET逻辑来确定请求的会话状态行为。
Required:为请求启用完全的读写会话状态行为。
Readonly:为请求启用只读会话状态。
Disabled:禁用会话状态
对于Default来说,ASP.NET通过映射的HttpHandler类型是否实现了相关接口来决定具体的会话状态控制。在System.Web.SessionState命名空间下定义了IRequestSessionState和IReadOnlySessionState接口。如下:
1 public interface IRequestSessionState 2 {} 3 4 public interface IReadOnlySessionState:IRequestSessionState 5 {}
View Code
如果HttpHandler实现了接口IRquestSessionState,则意味着采用Readonly模式,如果只实现了IRequestSessionState则采用Required模式。
具体采用何种会话状态行为取决于当前Http上下文(HttpContext.Current)。ASP.NET4.0为HttpContext定义了一个SetSessionStateBehavior方法,我们可以通过这个方法实现自由选择会话状态行为模式。HttpContextBase的子类HttpContextWrapper重写了这个方法。
1 public sealed class HttpContext:IServiceProvider,IPrincipalContainer 2 { 3 public void SetSessionStateBehavior(SessionStateBehavior session); 4 } 5 6 7 public class HttpContextBase:IServiceProvider 8 { 9 public void SetSessionStateBehavior(SessionStateBehavior session); 10 }
View Code
2.5 ControllerBase
用于激活Controller对象的ControllerFactory最终通过ControllerBuilder注册到MVC中,如下面代码所示,ControllerBuilder定义了一个静态只读属性Current用于返回当前的ControllerBuilder对象,这是针对整个web应用的全局对象。两个SetControllerFactory方法重载用于注册ControllerFactory的类型或实例,而GetControllerFactory则返回一个具体的ControllerFactory对象。
1 public class ControllerBuilder 2 { 3 public IControllerFactory GetControllerFactory(); 4 public void SetControllerFactory(Type controllerFactoryType); 5 public void SetControllerFactory(IControllerFactory controllerFactory); 6 7 public HashSet<string> DefaultNamespace{get;} 8 public static ControllerFactory Current{get;} 9 }
View Code
我们使用注册的ControllerFactory的类型,那么GetControllerFactory在执行的时候会通过对注册类型的反射(调用Activator的静态方法CreateInstance)来创建具体的ControllerFactory。如果注册的是一个具体的ControllerFactory对象,该对象直接从GetControllerFactory返回。
被ASP.NET路由系统进行拦截处理后生产一个用于封装路由信息的RouteData对象,而目标Controller的名称就包含在通过该RouteData的Values属性表示的RouteValueDictionary对象中对应的Key为”controller”。在默认情况下,这个作为路由数据的名称只能帮我们解析出Controller的类型名称,如果我们在不同的命名空间下定义了多个同名的Controller类,会导致激活系统无法确定具体的Controller的类型从而抛出异常。
为解决这个问题, 我们必须为定义同名的Controller类型的命名空间设置不同的优先级,具体说我们有两种提升命名空间优先级的方式:
1.在调用ROuteCollection的扩展方法MapRoute时,指定一个命名空间的列表.。通过这种方式指定的命名空间列表会保存在Route对象的DataTokens属性表示的RouteValueDictionary字典中,对应的Key为”Namespace”。
2.将其添加到当前的ControllerBuilder中的默认命名空间列表中,从上面的ControllerBuilder的定义可以看出,他具有一个HashSet<string>类型的只读属性DefaultNamespaces就代表 这么一个默认命名空间列表。
用于辅助解析Controller类新的命名空间分为三个梯队,简称为路由命名空间、ConrollerBuilder命名空间和Controller类型命名空间。如果一个梯队不能正确解析出目标Controller的类型,会将后面一个梯队的命名空间作为后备。
为了让读者对此如何提升命名空间优先级具有一个深刻的印象,我们来进行一个简单的实例演示。我们使用Visual Studio提供的项目模板创建一个空的ASP.NET MVC应用,并且使用如下所示的默认路由注册代码。
1 public class MvcApplication : System.Web.HttpApplication 2 { 3 public static void RegisterRoutes(RouteCollection routes) 4 { 5 routes.MapRoute( 6 name: "Default", 7 url: "{controller}/{action}/{id}", 8 defaults: new { controller = "Home", action = "Index", 9 id = UrlParameter.Optional } 10 ); 11 } 12 protected void Application_Start() 13 { 14 //其他操作 15 RegisterRoutes(RouteTable.Routes); 16 } 17 } 18 public class MvcApplication : System.Web.HttpApplication 19 { 20 public static void RegisterRoutes(RouteCollection routes) 21 { 22 routes.MapRoute( 23 name: "Default", 24 url: "{controller}/{action}/{id}", 25 defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional } 26 ); 27 } 28 protected void Application_Start() 29 { 30 //其他操作 31 RegisterRoutes(RouteTable.Routes); 32 } 33 }
View Code
然后我们在Controllers目录下添加一个.cs 文件,并在该文件中定义两个同名的Controller类。如下面的代码片断所示,这两个HomeCotroller类分别定义在命名空间Artech.MvcApp和Artech.MvcApp.Controllers之中,而Index操作返回的是一个将Controller类型全名为内容的ContentResult对象。
1 namespace Artech.MvcApp.Controllers 2 { 3 public class HomeController : Controller 4 { 5 public ActionResult Index() 6 { 7 return this.Content(this.GetType().FullName); 8 } 9 } 10 } 11 namespace Artech.MvcApp 12 { 13 public class HomeController : Controller 14 { 15 public ActionResult Index() 16 { 17 return this.Content(this.GetType().FullName); 18 } 19 } 20 }
View Code
现在我们直接运行该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 7 ControllerBuilder.Current.DefaultNamespaces.Add("Artech.MvcApp.Controllers"); 8 } 9 }
View Code
对用同时匹配注册的路由规则的两个HomeController,由于“Artech.MvcApp.Controllers”命名空间具有更高的匹配优先级,所有定义其中的HomeController会被选用,这可以通过如下图所示的运行结果看出来。
为了检验在路由注册时指定的命名空间和作为当前ControllerBuilder的命名空间哪个具有更高匹配优先级,我们修改定义在Global.asax中的路由注册代码。如下面的代码片断所示,我们在调用RouteTable的静态属性Routes的MapRoute方法进行路由注册的时候指定了命名空间(“Artech.MvcApp”)。
1 public class MvcApplication : System.Web.HttpApplication 2 { 3 public static void RegisterRoutes(RouteCollection routes) 4 { 5 routes.MapRoute( 6 name: "Default", 7 url: "{controller}/{action}/{id}", 8 defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }, 9 namespaces:new string[]{"Artech.MvcApp"} 10 ); 11 } 12 13 protected void Application_Start() 14 { 15 //其他操作 16 RegisterRoutes(RouteTable.Routes); 17 ControllerBuilder.Current.DefaultNamespaces.Add("Artech.MvcApp.Controllers"); 18 } 19 }
View Code
再次运行我们的程序会在浏览器中得到如图3-3所示的结果,从中可以看出定义在命名空间“Artech.MvcApp”中的HomeController被最终选用,可见较之作为当前ControllerBuilder的默认命名空间,在路由注册过程中执行的命名空间具有更高的匹配优先级,前者可以视为后者的一种后备。