我们将整个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对其进行释放清理工作。