ASP.NET MVC Controller激活系统详解:总体设计
我们将整个ASP.NET MVC框架划分为若干个子系统,那么针对请求上下文激活目标Controller对象的子系统被我们成为Controller激活系统。在正式讨论Controller对象具体是如何被创建爱之前,我们先来看看Controller激活系统在ASP.NET MVC中的总体设计,了解一下组成该子系统的一些基本的组件,以及它们对应的接口或者抽象类是什么。
目录
一、Controller
二、 ControllerFactory
三、ControllerBuilder
实例演示:如何提升命名空间的优先级
针对Area的路由对象的命名空间
四、 Controller的激活与URL路由
一、Controller
我们知道作为Controller的类型直接或者间接实现了IController接口。如下面的代码片断所示,IController接口仅仅包含一个参数类型为RequestContext的Execute方法。当一个Controller对象被激活之后,核心的操作就是根据请求上下文解析出目标Action方法,并通过Model绑定机制从请求上下文中提取相应的数据映射为方法的参数并最终执行Action方法。所有的这些操作都是调用这个Execute方法来执行的。
1: public interface IController
2: {
3: void Execute(RequestContext requestContext);
4: }
定义在IController接口中的Execute是以同步的方式执行的。为了支持以异步方式对请求的处理,IController接口的异步版本System.Web.Mvc.IAsyncController被定义出来。如下面的代码片断所示,实现了IAsyncController接口的异步Controller的执行通过BeginExecute/EndExecute方法组合来完成。
1: public interface IAsyncController : IController
2: {
3: IAsyncResult BeginExecute(RequestContext requestContext, AsyncCallback callback, object state);
4: void EndExecute(IAsyncResult asyncResult);
5: }
抽象类ControllerBase实现了IController接口,它具有如下几个重要的属性。TemplateData、ViewBag和ViewData用于存储从Controller向View传递的数据或者变量。其中TemplateData和ViewData具有基于字典的数据结构,Key和Value分别表示变量的名称和值,所不同的前者用于存储基于当前HTTP上下文的变量(在完成当前请求后,存储的数据会被回收)。ViewBag和ViewData具有相同的作用,甚至对应着相同的数据存储,它们之间的不同之处在于前者是一个动态对象,我们可以为其指定任意属性。
1: public abstract class ControllerBase : IController
2: {
3: //其他成员
4: public ControllerContext ControllerContext { get; set; }
5: public TempDataDictionary TempData { get; set; }
6: public object ViewBag { [return: Dynamic] get; }
7: public ViewDataDictionary ViewData { get; set; }
8: }
在ASP.NET MVC中我们会陆续遇到一系列的上下文(Context)对象,之前我们已经对表示请求上下文的RequestContext(HttpContext + RouteData)进行了详细的介绍,现在我们来介绍另一个具有如下定义的上下文类型ControllerContext。
1: public class ControllerContext
2: {
3: //其他成员
4: public ControllerContext();
5: public ControllerContext(RequestContext requestContext, ControllerBase controller);
6: public ControllerContext(HttpContextBase httpContext,
7: RouteData routeData, ControllerBase controller);
8:
9: public virtual ControllerBase Controller { get; set; }
10: public RequestContext RequestContext { get; set; }
11: public virtual HttpContextBase HttpContext { get; set; }
12: public virtual RouteData RouteData { get; set; }
13: }
顾名思义,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个接口。
1: public abstract class Controller :
2: ControllerBase,
3: IController,
4: IAsyncController,
5: IActionFilter,
6: IAuthorizationFilter,
7: IExceptionFilter,
8: IResultFilter,
9: IDisposable,
10: ...
11: {
12: //省略成员
13: }
二、 ControllerFactory
ASP.NET MVC为Controller的激活定义相应的相应的工厂,我们将其统称为ControllerFactory,所有的ControllerFactory实现了接口IControllerFactory接口。如下面的代码片断所示,Controller对象的激活最终最终通过IControllerFactory的CreateController方法来完成,该方法的两个参数分别表示当前请求上下文和从路由信息中获取的Controller的名称(最初来源于请求地址)。
1: public interface IControllerFactory
2: {
3: IController CreateController(RequestContext requestContext, string controllerName);
4: SessionStateBehavior GetControllerSessionBehavior(RequestContext requestContext, string controllerName);
5: void ReleaseController(IController controller);
6: }
7: public enum SessionStateBehavior
8: {
9: Default,
10: Required,
11: ReadOnly,
12: Disabled
13: }
处理负责创建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的同名方法。
1: public sealed class HttpContext : IServiceProvider, IPrincipalContainer
2: {
3: //其他成员
4: public void SetSessionStateBehavior(
5: SessionStateBehavior sessionStateBehavior);
6: }
7: public class HttpContextBase: IServiceProvider
8: {
9: //其他成员
10: public void SetSessionStateBehavior(SessionStateBehavior sessionStateBehavior);
11: }
三、ControllerBuilder
用于激活Controller对象的ControllerFactory最终通过ControllerBuilder注册到ASP.NET 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> DefaultNamespaces { get; }
8: public static ControllerBuilder Current { get; }
9: }
具体来说,如果我们是注册的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”。
1: public static class RouteCollectionExtensions
2: {
3: //其他成员
4: public static Route MapRoute(this RouteCollection routes, string name, string url, string[] namespaces);
5: public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, string[] namespaces);
6: public static Route MapRoute(this RouteCollection routes, string name, string url, object defaults, object constraints, string[] namespaces);
7: }
而另一种提升命名空间优先级的方式就是将其添加到当前的ControllerBuilder中的默认命名空间列表中。从上面的给出的ControllerBuilder的定义可以看出,它具有一个HashSet<string>类型的只读属性DefaultNamespaces就代表了这么一个默认命名空间列表。对于这两种不同的命名空间优先级提升方式,前者(通过路由注册)指定命名空间具有更高的优先级。
实例演示:如何提升命名空间的优先级
为了让读者对此如何提升命名空间优先级具有一个深刻的印象,我们来进行一个简单的实例演示。我们使用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: }
然后我们在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: }
现在我们直接运行该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”)。
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: }
再次运行我们的程序会在浏览器中得到如图3-3所示的结果,从中可以看出定义在命名空间“Artech.MvcApp”中的HomeController被最终选用,可见较之作为当前ControllerBuilder的默认命名空间,在路由注册过程中执行的命名空间具有更高的匹配优先级,前者可以视为后者的一种后备。[源代码从这里下载]
在路由注册时指定的命名空间比当前ControllerBuilder的默认命名空间具有更高的匹配优先级,但是对于这两个集合中的所有命名空间却具有相同的匹配优先级。换句话说,用于辅助解析Controller类新的命名空间分为三个梯队,简称为路由命名空间、ConrollerBuilder命名空间和Controller类型命名空间,如果前一个梯队不能正确解析出目标Controller的类型,则将后一个梯队的命名空间作为后备;反之,如果根据某个梯队的命名空间进行解析得到多个匹配的Controller类型,会直接抛出异常。
现在我们对本例的路由注册代码作了如下的修改,为注册的路由对象指定了两个命名空间(分别是两个HomeContrller所在的命名空间),运行我们的程序依然会得到如第一张图所示的错误。
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", "Artech.MvcApp.Controllers" }
10: );
11: }
12:
13: protected void Application_Start()
14: {
15: //其他操作
16: RegisterRoutes(RouteTable.Routes);
17: }
18: }
针对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。
1: namespace Artech.MvcApp.Areas
2: {
3: public class HomeController : Controller
4: {
5: public ActionResult Index()
6: {
7: return Content("...");
8: }
9: }
10: }
现在我们在浏览器中通过匹配的URL(/Admin/Home/Index)来访问Area为Admin的HomeController的Index操作,会得到如下图所示的HTTP状态为404的错误。这就是因为在对Controller类型进行解析的时候是严格按照对应的AreaRegistration所在命名空间来进行的,很显然在这个范围内是不可能找得到对应的Controller类型的。[源代码从这里下载]
四、Controller的激活与URL路由
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方法获取。
1: public class MvcRouteHandler : IRouteHandler
2: {
3: private IControllerFactory _controllerFactory;
4: public MvcRouteHandler(): this(ControllerBuilder.Current.GetControllerFactory())
5: { }
6: public MvcRouteHandler(IControllerFactory controllerFactory)
7: {
8: _controllerFactory = controllerFactory;
9: }
10: IHttpHandler IRouteHandler.GetHttpHandler(RequestContext requestContext)
11: {
12: string controllerName = (string)requestContext.RouteData.GetRequiredString("controller");
13: SessionStateBehavior sessionStateBehavior = _controllerFactory.GetControllerSessionBehavior(requestContext, controllerName);
14: requestContext.HttpContext.SetSessionStateBehavior(sessionStateBehavior);
15:
16: return new MvcHandler(requestContext);
17: }
18: }
在用于提供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属性,该属性在构造函数中被初始化。
1: public class MvcHandler : IHttpHandler
2: {
3: public RequestContext RequestContext { get; private set; }
4: public bool IsReusable
5: {
6: get { return false; }
7: }
8: public MvcHandler(RequestContext requestContext)
9: {
10: this.RequestContext = requestContext;
11: }
12: public void ProcessRequest(HttpContext context)
13: {
14: IControllerFactory controllerFactory = ControllerBuilder.Current.GetControllerFactory();
15: string controllerName = this.RequestContext.RouteData.GetRequiredString("controller");
16: IController controller = controllerFactory.CreateController(this.RequestContext, controllerName);
17: try
18: {
19: controller.Execute(this.RequestContext);
20: }
21: finally
22: {
23: controllerFactory.ReleaseController(controller);
24: }
25: }
26: }
在ProcessRequest方法中,通过RequestContext对象得到目标Controller的名称,并通过它利用当前ControllerBuilder创建的ControllerFactory激活Controller对象。在执行了被激活Controller对象的Execute方法之后调用ControllerFactory的ReleaseController对其进行释放清理工作。