一.总体设计
我们将整个ASP.NET MVC框架划分为若干个子系统,那么针对请求上下文激活目标Controller对象的子系统被我们成为Controller激活系统。在正式讨论Controller对象具体是如何被创建爱之前,我们先来看看Controller激活系统在ASP.NET MVC中的总体设计,了解一下组成该子系统的一些基本的组件,以及它们对应的接口或者抽象类是什么。
1、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: }
2、 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: }
3、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应该被选用,所以会出现如下图所示的错误。[源代码从这里下载]
![](https://img-my.csdn.net/uploads/201212/27/1356573177_5572.png)
目前定义了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会被选用,这可以通过如下图所示的运行结果看出来。[源代码从这里下载]
![](https://img-my.csdn.net/uploads/201212/27/1356573225_5301.png)
为了检验在路由注册时指定的命名空间和作为当前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的默认命名空间,在路由注册过程中执行的命名空间具有更高的匹配优先级,前者可以视为后者的一种后备。[源代码从这里下载]
![](https://img-my.csdn.net/uploads/201212/27/1356573462_5584.png)
在路由注册时指定的命名空间比当前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类型的。[源代码从这里下载]
![](https://img-my.csdn.net/uploads/201212/27/1356573726_4462.png)
4、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对其进行释放清理工作。
二 .默认实现
Controller激活系统最终通过注册的ControllerFactory创建相应的Conroller对象,如果没有对ControllerFactory类型或者类型进行显式注册(通过调用当前ControllerBuilder的SetControllerFactory方法),默认使用的是一个DefaultControllerFactory对象,我们现在就来讨论实现在DefaultControllerFactory类型中的默认Controller激活机制。
1、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: }
2、 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>
3、 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: }
4、会话状态行为的控制
至于用于返回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: }
三.IoC的应用
所谓控制反转(IoC: Inversion Of Control)简单地说就是应用本身不负责依赖对象的创建和维护,而交给一个外部容器来负责。这样控制权就由应用转移到了外部IoC容器,控制权就实现了所谓的反转。比如在类型A中需要使用类型B的实例,而B实例的创建并不由A来负责,而是通过外部容器来创建。通过IoC的方式是实现针对目标Controller的激活具有重要的意义。
1、从Unity来认识IoC
有时我们又将IoC称为依赖注入(DI: Dependency Injection)。所谓依赖注入,就是由外部容器在运行时动态地将依赖的对象注入到组件之中。Martin Fowler在那篇著名的文章《Inversion of Control Containers and the Dependency Injection pattern》中将具体依赖注入划分为三种形式,即构造器注入、属性(设置)注入和接口注入,而我个人习惯将其划分为一种(类型)匹配和三种注入:
- 类型匹配(Type Matching):虽然我们通过接口(或者抽象类)来进行服务调用,但是服务本身还是实现在某个具体的服务类型中,这就需要某个类型注册机制来解决服务接口和服务类型之间的匹配关系;
- 构造器注入(Constructor Injection):IoC容器会智能地选择选择和调用适合的构造函数以创建依赖的对象。如果被选择的构造函数具有相应的参数,IoC容器在调用构造函数之前解析注册的依赖关系并自行获得相应参数对象;
- 属性注入(Property Injection):如果需要使用到被依赖对象的某个属性,在被依赖对象被创建之后,IoC容器会自动初始化该属性;
- 方法注入(Method Injection):如果被依赖对象需要调用某个方法进行相应的初始化,在该对象创建之后,IoC容器会自动调用该方法。
开源社区具有很有流行的IoC框架,比如Castle Windsor、Unity、Spring.NET、StructureMap和Ninject等。Unity是微软Patterns & Practices部门开发的一个轻量级的IoC框架。该项目在Codeplex上的地址为http://unity.codeplex.com/, 你可以下载相应的安装包和开发文档。Unity的最新版本为2.1。出于篇幅的限制,我不可能对Unity进行前面的介绍,但是为了让读者了解IoC在Unity中的实现,我写了一个简单的程序。
我们创建一个控制台程序,定义如下几个接口(IA、IB、IC和ID)和它们各自的实现类(A、B、C、D)。在类型A中定义了3个属性B、C和D,其类型分别为接口IB、IC和ID。其中属性B在构在函数中被初始化,以为着它会以构造器注入的方式被初始化;属性C上应用了DependencyAttribute特性,意味着这是一个需要以属性注入方式被初始化的依赖属性;属性D则通过方法Initialize初始化,该方法上应用了特性InjectionMethodAttribute,意味着这是一个注入方法在A对象被IoC容器创建的时候会被自动调用。
1: namespace UnityDemo
2: {
3: public interface IA { }
4: public interface IB { }
5: public interface IC { }
6: public interface ID {}
7:
8: public class A : IA
9: {
10: public IB B { get; set; }
11: [Dependency]
12: public IC C { get; set; }
13: public ID D { get; set; }
14:
15: public A(IB b)
16: {
17: this.B = b;
18: }
19: [InjectionMethod]
20: public void Initialize(ID d)
21: {
22: this.D = d;
23: }
24: }
25: public class B: IB{}
26: public class C: IC{}
27: public class D: ID{}
28: }
然后我们为该应用添加一个配置文件,并定义如下一段关于Unity的配置。这段配置定义了一个名称为defaultContainer的Unity容器,并在其中完成了上面定义的接口和对应实现类之间映射的类型匹配。
1: <configuration>
2: <configSections>
3: <section name="unity"
4: type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
5: Microsoft.Practices.Unity.Configuration"/>
6: </configSections>
7: <unity>
8: <containers>
9: <container name="defaultContainer">
10: <register type="UnityDemo.IA, UnityDemo" mapTo="UnityDemo.A, UnityDemo"/>
11: <register type="UnityDemo.IB, UnityDemo" mapTo="UnityDemo.B, UnityDemo"/>
12: <register type="UnityDemo.IC, UnityDemo" mapTo="UnityDemo.C, UnityDemo"/>
13: <register type="UnityDemo.ID, UnityDemo" mapTo="UnityDemo.D, UnityDemo"/>
14: </container>
15: </containers>
16: </unity>
17: </configuration>
最后在Main方法中创建一个代表IoC容器的UnityContainer对象,并加载配置信息对其进行初始化。然后调用它的泛型的Resolve方法创建一个实现了泛型接口IA的对象。最后将返回对象转变成类型A,并检验其B、C和D属性是否是空。
1: static void Main(string[] args)
2: {
3: IUnityContainer container = new UnityContainer();
4: UnityConfigurationSection configuration = ConfigurationManager.GetSection(UnityConfigurationSection.SectionName)
5: as UnityConfigurationSection;
6: configuration.Configure(container, "defaultContainer");
7: A a = container.Resolve<IA>() as A;
8: if (null != a)
9: {
10: Console.WriteLine("a.B == null ? {0}", a.B == null ? "Yes" : "No");
11: Console.WriteLine("a.C == null ? {0}", a.C == null ? "Yes" : "No");
12: Console.WriteLine("a.D == null ? {0}", a.D == null ? "Yes" : "No");
13: }
14: }
从如下给出的执行结果我们可以得到这样的结论:通过Resolve<IA>方法返回的是一个类型为A的对象,该对象的三个属性被进行了有效的初始化。这个简单的程序分别体现了接口注入(通过相应的接口根据配置解析出相应的实现类型)、构造器注入(属性B)、属性注入(属性C)和方法注入(属性D)。[源代码从这里下载]
1: a.B == null ? No
2: a.C == null ? No
3: a.D == null ? No
2、Controller与Model的分离
在《MVC、MVP以及Model2》中我们谈到ASP.NET MVC是基于MVC的变体Model2设计的。ASP.NET MVC所谓的Model仅仅表示绑定到View上的数据,我们一般称之为View Model。而真正的Model一般意义上指维护应用状态和提供业务功能操作的领域模型,或者是针对业务层的入口或者业务服务的代理。真正的MVC在ASP.NET MVC中的体现如下图所示。
![](https://img-my.csdn.net/uploads/201212/27/1356574158_5569.png)
对于一个ASP.NET MVC应用来说,用户交互请求直接发送给Controller。如果涉及到针对某个个业务功能的调用,Controller会直接调用Model;如果呈现业务数据,Controller会通过Model获取相应业务数据并转换成View Model,最终通过View呈现出来。这样的交互协议方式反映了Controller针对Model的直接依赖。
如果我们在Controller激活系统中引入IoC,并采用IoC的方式提供用于处理请求的Controller对象,那么Controller和Model之间的依赖程度在很大程度上降低。我们甚至可以像下图所示的一样,以接口的方式都Model进行抽象,让Controller依赖于这个抽象化的Model接口,而不是具体的Model实现。
3、 创建基于IoC的自定义ControllerFactory
ASP.NET MVC的Controller激活系统最终通过ControllerFactory来创建目标Controller对象,要将IoC引入ASP.NET MVC并通过对应的IoC容器实现对目标Controller的激活,我们很自然地会想到自定义一个基于IoC的ControllerFactory。
对于IoC的ControllerFactory的创建,我们可以直接实现IControllerFactory接口创建一个全新的ControllerFactory类型,这需要实现包括Controller类型的解析、Controller实例的创建与释放以及会话状态行为选项的获取在内的所有功能。一般来说,Controller实例的创建与释放才收IoC容器的控制,为了避免重新实现其他的功能,我们可以直接继承DefaultControllerFactory,重写Controller实例创建于释放的逻辑。
实例演示:自定义一个基于Unity的ControllerFactory
现在我们通过一个简单的实例演示如何通过自定义ControllerFactory利用Unity进行Controller的激活与释放。为了避免针对Controller类型解析和会话状态行为选项的获取逻辑的重复定义,我们直接继承DefaultControllerFactory。我们将该自定义ControllerFactory命名为UnityControllerFactory,整个定义如下面的的代码片断所示。[源代码从这里下载]
1: public class UnityControllerFactory : DefaultControllerFactory
2: {
3: static object syncHelper = new object();
4: static Dictionary<string, IUnityContainer> containers = new Dictionary<string,IUnityContainer>();
5: public IUnityContainer UnityContainer { get; private set; }
6: public UnityControllerFactory(string containerName = "")
7: {
8: if (containers.ContainsKey(containerName))
9: {
10: this.UnityContainer = containers[containerName];
11: return;
12: }
13: lock (syncHelper)
14: {
15: if (containers.ContainsKey(containerName))
16: {
17: this.UnityContainer = containers[containerName];
18: return;
19: }
20: IUnityContainer container = new UnityContainer();
21: //配置UnityContainer
22: UnityConfigurationSection configSection = ConfigurationManager.GetSection(UnityConfigurationSection.SectionName)
23: as UnityConfigurationSection;
24: if (null == configSection && !string.IsNullOrEmpty(containerName))
25: {
26: throw new ConfigurationErrorsException("The <unity> configuration section does not exist.");
27: }
28: if (null != configSection )
29: {
30: if(string.IsNullOrEmpty(containerName))
31: {
32: configSection.Configure(container);
33: }
34: else
35: {
36: configSection.Configure(container, containerName);
37: }
38: }
39: containers.Add(containerName, container);
40: this.UnityContainer = containers[containerName];
41: }
42: }
43: protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
44: {
45: if (null == controllerType)
46: {
47: return null;
48: }
49: return (IController)this.UnityContainer.Resolve(controllerType);
50: }
51: public override void ReleaseController(IController controller)
52: {
53: this.UnityContainer.Teardown(controller);
54: }
55: }
UnityControllerFactory的UnityConainer属性表示的实现自Microsoft.Practices.Unity.IUnityContainer接口的对象表示定义在Unity中的IoC容器。为了避免UnityConainer对象的频繁创建,我们创建的UnityConainer对象保存在一个通过静态字段(containers)表示的字典对象中,其Key为UnityConainer的配置名称。构造函数中的参数containnerName表示使用的UnityConainer的配置名称,如果静态字典中存在着与之匹配的UnityConainer对象,则直接获取出来作为UnityConainer属性的值;否则创建一个新的UnityConainer对象并加载对应的配置对其进行相关设置,最后将其赋值给UnityConainer属性并添加到静态字典之中。
我们重写了定义在基类DefaultControllerFactory的虚方法GetControllerInstance,在解析出来的Controller类型(controllerType参数)不为Null的情况下,直接调用UnityConainer的Resolve方法激活对应的Controller实例。在用于释放Controller对象的ReleaseController方法中,我们直接将Controller对象作为参数调用UnityConainer的Teardown方法。
整个自定义的UnityControllerFactory就这么简单,为了演示IoC在它身上的体现,我们在一个简单的ASP.MVC实例中来使用我们刚刚定义的UnityControllerFactory。我们沿用在《ASP.NET的路由系统》中使用过的关于“员工管理”的场景,如下图所示,本实例由两个页面(对应着两个View)组成,一个用于显示员工列表,另一个用于显示基于某个员工的详细信息。
![](https://img-my.csdn.net/uploads/201212/27/1356574217_1390.png)
我们通过Visual Studio的ASP.NET MVC项目模板创建一个空的Web应用,并添加针对Unity的两个程序集(Microsoft.Practices.Unity.dll和Microsoft.Practices.Unity.Configuration.dll)引用。然后我们再Models目录下定义如下一个表示员工信息的Employee类型。
1: public class Employee
2: {
3: [Display(Name="ID")]
4: public string Id { get; private set; }
5: [Display(Name = "姓名")]
6: public string Name { get; private set; }
7: [Display(Name = "性别")]
8: public string Gender { get; private set; }
9: [Display(Name = "出生日期")]
10: [DataType(DataType.Date)]
11: public DateTime BirthDate { get; private set; }
12: [Display(Name = "部门")]
13: public string Department { get; private set; }
14:
15: public Employee(string id, string name, string gender, DateTime birthDate, string department)
16: {
17: this.Id = id;
18: this.Name = name;
19: this.Gender = gender;
20: this.BirthDate = birthDate;
21: this.Department = department;
22: }
23: }
我们创建一个独立的组件来模拟用于维护应用状态提供业务操作功能的Model(在这里我们将ASP.NET MVC中的Model视为View Model),为了降低Controller和Model之间耦合度,我们为这个Model定义了接口。如下所示的IEmployeeRepository就代表了这个接口,唯一的方法GetEmployees用于获取所有员工列表(id参数值为空)或者基于指定ID的某个员工信息。
1: public interface IEmployeeRepository
2: {
3: IEnumerable<Employee> GetEmployees(string id = "");
4: }
EmployeeRepository类型实现了IEmployeeRepository接口,具体的定义如下所示。简单起见,我们直接通过一个类型为List<Employee>得静态字段来表示所有员工信息的存储。
1: public class EmployeeRepository: IEmployeeRepository
2: {
3: private static IList<Employee> employees;
4: static EmployeeRepository()
5: {
6: employees = new List<Employee>();
7: employees.Add(new Employee(Guid.NewGuid().ToString(), "张三", "男", new DateTime(1981, 8, 24), "销售部"));
8: employees.Add(new Employee(Guid.NewGuid().ToString(), "李四", "女", new DateTime(1982, 7, 10), "人事部"));
9: employees.Add(new Employee(Guid.NewGuid().ToString(), "王五", "男", new DateTime(1981, 9, 21), "人事部"));
10: }
11: public IEnumerable<Employee> GetEmployees(string id = "")
12: {
13: return employees.Where(e => e.Id == id || string.IsNullOrEmpty(id));
14: }
15: }
现在我们来创建我们的Controller,在这里我们将其起名为EmployeeController。如下面的代码片断所示,EmployeeController具有一个类型为IEmployeeRepository的属性Repository,应用在上面的DependencyAttribute特性我们知道这是一个“依赖属性”,如果采用UnityContainer来激活EmployeeController对象的时候,会根据注册的类型映射来实例化一个实现了IEmployeeRepository的类型的实例来初始化该属性。
1: public class EmployeeController : Controller
2: {
3: [Dependency]
4: public IEmployeeRepository Repository { get; set; }
5: public ActionResult Index()
6: {
7: var employees = this.Repository.GetEmployees();
8: return View(employees);
9: }
10: public ActionResult Detail(string id)
11: {
12: Employee employee = this.Repository.GetEmployees(id).FirstOrDefault();
13: if (null == employee)
14: {
15: throw new HttpException(404, string.Format("ID为{0}的员工不存在", id));
16: }
17: return View(employee);
18: }
19: }
默认的Index操作方法中,我们通过Repository属性获取表示所有员工的列表,并将其作为Model显现在对应的View中。至于用于显示指定员工ID详细信息的Detail操作,我们同样通过Repository属性根据指定的ID获取表示相应员工信息的Employee对象,如果该对象为Null,直接返回一个状态为404的HttpException;否则作为将其作为Model显示在相应的View中。
如下所示的名为Index的View的定义,它的Model类型为IEnumerable<Employee>,在这里View中,我们通过一个表格来显示表示为Model的员工列表。值得一提的是,我们通过调用HtmlHelper的ActionLink方法将员工的名称显示为一个执行Detail操作的连接,作为路由变量参数集合中同时包含当前员工的ID和姓名。根据我们即将注册的路由规则,这个链接地址的格式为/Employee/Detail/{Name}/{Id}。
1: @model IEnumerable<Employee>
2: @{
3: ViewBag.Title = "Index";
4: }
5: <table id="employees" rules="all" border="1">
6: <tr>
7: <th>姓名</th>
8: <th>性别</th>
9: <th>出生日期</th>
10: <th>部门</th>
11: </tr>
12: @{
13: foreach(Employee employee in Model)
14: {
15: <tr>
16: <td>@Html.ActionLink(employee.Name, "Detail",
17: new { name = employee.Name, id = employee.Id })</td>
18: <td>@employee.Gender</td>
19: <td>@employee.BirthDate.ToString("dd/MM/yyyy")</td>
20: <td>@employee.Department</td>
21: </tr>
22: }
23: }
24: </table>
用于显示具有某个员工信息的名为Detail的View定义如下,这是一个Model类型为Employee的强类型的View,我们通过通过表格的形式将员工的详细信息显示出来。
1: @model Employee
2: @{
3: ViewBag.Title = Model.Name;
4: }
5: <table id="employee" rules="all" border="1">
6: <tr><td>@Html.LabelFor(m=>m.Id)</td><td>@Model.Id</td></tr>
7: <tr><td>@Html.LabelFor(m=>m.Name)</td><td>@Model.Name</td></tr>
8: <tr><td>@Html.LabelFor(m=>m.Gender)</td><td>@Model.Gender</td></tr>
9: <tr><td>@Html.LabelFor(m=>m.BirthDate)</td><td>@Model.BirthDate.ToString("dd/MM/yyyy")</td></tr>
10: <tr><td>@Html.LabelFor(m=>m.Department)</td><td>@Model.Department</td></tr>
11: </table>
我需要在Global.asax中完成两件事情,即针对自定义UnityControllerFactory和路由的注册。如下的代码片断所示,在Application_Start方法中我们通过当前ControllerBuilder注册了一个UnityControllerFactory实例。 在RegisterRoutes方法中我们注册两个路由,前者针对Detail操作(URL模版包含员工的ID和姓名),后者针对Index操作。
1: public class MvcApplication : System.Web.HttpApplication
2: {
3: //其他成员
4: public static void RegisterRoutes(RouteCollection routes)
5: {
6: routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
7: routes.MapRoute(
8: name: "Detail",
9: url: "{controller}/{action}/{name}/{id}",
10: defaults: new { controller = "Employee" }
11: );
12: routes.MapRoute(
13: name: "Default",
14: url: "{controller}/{action}",
15: defaults: new { controller = "Employee", action = "Index"}
16: );
17: }
18:
19: protected void Application_Start()
20: {
21: //其他操作
22: RegisterRoutes(RouteTable.Routes);
23: ControllerBuilder.Current.SetControllerFactory(new UnityControllerFactory());
24: }
25: }
EmployeeController仅仅依赖于IEmployeeRepository接口,它通过基于该类型的依赖属性Repository返回员工信息,我们需要通过注册为之设置一个具体的匹配类型,而这个类型自然就是前面我们定义的EmployeeRepository。如下所示的正是Unity相关的类型注册配置。到此为止,整个实例的编程和配置工作既已完成(忽略了针对样式的设置),运行该程序就可以得到如上图所示的效果。
1: <configuration>
2: <configSections>
3: <section name="unity"
4: type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection,
5: Microsoft.Practices.Unity.Configuration"/>
6: </configSections>
7: <unity>
8: <containers>
9: <container>
10: <register type="Artech.Mvc.IEmployeeRepository, Artech.Mvc.MvcApp"
11: mapTo="Artech.Mvc.EmployeeRepository, Artech.Mvc.MvcApp"/>
12: </container>
13: </containers>
14: </unity>
15: </configuration>
4、ControllerActivator V.S. DependencyResolver
如下面的代码片断所示,DefaultControllerFactory具有两个构造函数重载,其中一个具有一个类型为IControllerActivator接口的参数,我们将实现了该接口得类型统称为ControllerActivator。
1: public class DefaultControllerFactory : IControllerFactory
2: {
3: //其他成员
4: public DefaultControllerFactory();
5: public DefaultControllerFactory(IControllerActivator controllerActivator);
6: }
顾名思义,ControllerActivator就是Controller的“激活器”,Controller的激活实现在唯一的Create方法中。如下面的代码所示,该方法具有两个参数(requestContext和controllerType),分别代表当前请求上下文和解析出来的目标Controller的类型。
1: public interface IControllerActivator
2: {
3: IController Create(RequestContext requestContext, Type controllerType);
4: }
在默认的情况下(调用DefaultControllerFactory默认构造函数或者指定的参数为Null),Controller激活系统 会默认使用一个类型为DefaultControllerActivator的对象。如下面的代码片断所示,DefaultControllerActivator是一个实现了IControllerActivator私有类型而已,我们不能直接通过编程的方式使用它。
1: private class DefaultControllerActivator : IControllerActivator
2: {
3: public DefaultControllerActivator();
4: public DefaultControllerActivator(IDependencyResolver resolver);
5: public IController Create(RequestContext requestContext, Type controllerType);
6: }
DefaultControllerActivator的构造函数具有一个类型为IDependencyResolver的参数,这是一个重要的接口,我们将实现了该接口的类型统称为DependencyResolver。。如下面的代码片断所示,IDependencyResolver接口具有两个方法GetService和GetServices,用于根据指定的类型获取单个或者多个实例。实际上DefaultControllerActivator就是通过调用GetService方法获取具体的Controller对象的
1: public interface IDependencyResolver
2: {
3: object GetService(Type serviceType);
4: IEnumerable<object> GetServices(Type serviceType);
5: }
如果在构造DefaultControllerActivator对象的时候传入的参数为Null,那么Controller激活系统会使用通过DependencyResolver的静态只读属性Current表示DependencyResolver。需要提醒的是,DependencyResolver类型没有实现IDependencyResolver接口,而是对一个实现了IDependencyResolver接口类型对象的封装。
1: public class DependencyResolver
2: {
3: private static DependencyResolver _instance;
4:
5: public void InnerSetResolver(object commonServiceLocator);
6: public void InnerSetResolver(IDependencyResolver resolver);
7: public void InnerSetResolver(Func<Type, object> getService, Func<Type, IEnumerable<object>> getServices);
8:
9: public static void SetResolver(object commonServiceLocator);
10: public static void SetResolver(IDependencyResolver resolver);
11: public static void SetResolver(Func<Type, object> getService, Func<Type, IEnumerable<object>> getServices);
12:
13: public static IDependencyResolver Current { get; }
14: public IDependencyResolver InnerCurrent { get; }
15: }
这个被封装的DependencyResolver(指实现了接口IDependencyResolver的某个类型的类型,不是指DependencyResolver类型的对象,对于后者我会采用“DependencyResolver类型对象”的说法)通过只读属性InnerCurrent表示,而三个InnerSetResolver方法重载用于初始化改属性。静态字段_instance表示当前的DependencyResolver类型对象,静态只读属性Current则表示该对象内部封装的DependencyResolver对象,而它通过三个静态的SetResolver进行初始化。
如果我们不曾通过调用DependencyResolver的静态方法SetResolver通过Current属性表示的当前DependencyResolver进行显示设置,该属性默认返回一个DefaultDependencyResolver对象。如下面的代码片断所示,DefaultDependencyResolver是一个实现了IDependencyResolver接口的私有类型,在实现的GetService方法中,它直接通过根据指定的类型以反射的形式创建相应的对象并返回,所以前面我们说DefaultControllerFactory根据解析出来的Controller类型以反射的形式创建对应的实例在这里得到了印证。至于GetServices方法则返回一个空对象集合。
1: private class DefaultDependencyResolver : IDependencyResolver
2: {
3: public object GetService(Type serviceType)
4: {
5: if (serviceType.IsInterface || serviceType.IsAbstract)
6: {
7: return null;
8: }
9: try
10: {
11: return Activator.CreateInstance(serviceType);
12: }
13: catch
14: {
15: return null;
16: }
17: }
18:
19: public IEnumerable<object> GetServices(Type serviceType)
20: {
21: return Enumerable.Empty<object>();
22: }
23: }
上面介绍的类型DefaultControllerFactory、IControllerActivator、DefaultControllerActivator、IDependencyResolver、DefaultDependencyResolver和DependencyResolver之前的关系基本上可以通过如下图所示的类图来体现。
5、通过自定义ControllerActivator实现IoC
如果我们基于一个ControllerActivator对象来创建一个DefaultControllerFactory,它会最终被用于Controller对象的激活,那么我们可以自定义ControllerActivator的方式将IoC引入Controller激活系统。我们接下来自定义的ControllerActivtor基于另一个IoC框架Ninject,较之Unity,Ninject是一个更加轻量级也更适合ASP.NET MVC的IoC框架。我们将自定义的ControllerActivator起名为NinjectControllerActivator,全部定义如下。[源代码从这里下载]
1: public class NinjectControllerActivator: IControllerActivator
2: {
3: public IKernel Kernel { get; private set; }
4: public NinjectControllerActivator()
5: {
6: this.Kernel = new StandardKernel();
7: AddBindings();
8: }
9: public IController Create(RequestContext requestContext, Type controllerType)
10: {
11: return (IController)this.Kernel.TryGet(controllerType) as IController;
12: }
13: private void AddBindings()
14: {
15: this.Kernel.Bind<IEmployeeRepository>().To<EmployeeRepository>();
16: }
17: }
我们使用的还是上面演示的关于员工管理的例子。NinjectControllerActivator的只读属性Kernel在这里用于类型注册和基于类型的实例提供,具体来说它是在构造函数中初始化的StandardKernel对象。同样在构造函数中,我们通过该Kernel实现了作为Model接口的IEmployeeRepository类型和Model实现的EmployeeRepository类型之间的映射。在Create方法中,我们通过Kernel的TryGet方法根据指定的类型获取相应的Controller对象。
现在我们无须再使用自定义的ControllerFactory,只需要注册一个基于我们自定义的NinjectControllerActivator的DefaultControllerFactory即可。定义在Global.asax中与ControllerFactory注册相关的代码如下所示。
1: public class MvcApplication : System.Web.HttpApplication
2: {
3: //其他成员
4: protected void Application_Start()
5: {
6: //其他操作
7: NinjectControllerActivator controllerActivator = new NinjectControllerActivator();
8: DefaultControllerFactory controllerFactory = new DefaultControllerFactory(controllerActivator);
9: ControllerBuilder.Current.SetControllerFactory(controllerFactory);
10: }
11: }
6、通过自定义DependencyResoolver实现IoC
通过前面的介绍我们知道,当我们调用构造函数创建一个DefaultControllerFactory的时候,如果调用的时候默认无参构造函数,后者将作为参数的ControllerActivator对象设置为Null,那么默认请求用于激活Controller实例的是通过DependencyResoolver类型的静态属性Current表示的DependencyResoolver对象。换言之,我们可以通过自定义DependencyResoolver的方式来实现基于IoC的Controller激活。
同样是采用Ninject,我们定义了一个具有如下定义的NinjectDependencyResolver。与上面定义的NinjectControllerActivator类似,NinjectDependencyResolver具有一个IKernel类型的只读属性Kernel,该属性在构造函数中被初始化。同样是在构造函数中,我们通过该Kernel完成了IEmployeeRepository接口和EmployeeRepository类型的注册。对于实现的GetService和GetServices方法,我们直接调用Kernel的TryGet和GetAll返回指定类型的实例和实例列表。[源代码从这里下载]
1: public class NinjectDependencyResolver : IDependencyResolver
2: {
3: public IKernel Kernel { get; private set; }
4: public NinjectDependencyResolver()
5: {
6: this.Kernel = new StandardKernel();
7: AddBindings();
8: }
9: private void AddBindings()
10: {
11: this.Kernel.Bind<IEmployeeRepository>().To<EmployeeRepository>();
12: }
13:
14: public object GetService(Type serviceType)
15: {
16: return this.Kernel.TryGet(serviceType);
17: }
18:
19: public IEnumerable<object> GetServices(Type serviceType)
20: {
21: return this.Kernel.GetAll(serviceType);
22: }
23: }
由于默认情况下通过无参构造函数创建的DefaultConrtollerFactory会被使用,所以我们无须进行ControllerFactory的注册。我们只需要创建一个自定义的NinjectDependencyResolver对象并将其作为当前的DependencyResolver即可,定义在Global.asax设置当前DependencyResolver的代码如下所示。
1: public class MvcApplication : System.Web.HttpApplication
2: {
3: //其他成员
4: protected void Application_Start()
5: {
6: //其他操作
7: DependencyResolver.SetResolver( new NinjectDependencyResolver());
8: }
9: }