ASP.NET MVC Controller激活系统详解:IoC的应用[上篇]
所谓控制反转(IoC: Inversion Of Control)简单地说就是应用本身不负责依赖对象的创建和维护,而交给一个外部容器来负责。这样控制权就由应用转移到了外部IoC容器,控制权就实现了所谓的反转。比如在类型A中需要使用类型B的实例,而B实例的创建并不由A来负责,而是通过外部容器来创建。通过IoC的方式是实现针对目标Controller的激活具有重要的意义。
目录
一、从Unity来认识IoC
二、Controller与Model的分离
三、 创建基于IoC的自定义ControllerFactory
实例演示:自定义一个基于Unity的ControllerFactory
四、ControllerActivator V.S. DependencyResoolver
五、通过自定义ControllerActivator实现IoC
六、通过自定义DependencyResoolver实现IoC
一、从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
二、Controller与Model的分离
在《MVC、MVP以及Model2[下篇]》中我们谈到ASP.NET MVC是基于MVC的变体Model2设计的。ASP.NET MVC所谓的Model仅仅表示绑定到View上的数据,我们一般称之为View Model。而真正的Model一般意义上指维护应用状态和提供业务功能操作的领域模型,或者是针对业务层的入口或者业务服务的代理。真正的MVC在ASP.NET MVC中的体现如下图所示。
对于一个ASP.NET MVC应用来说,用户交互请求直接发送给Controller。如果涉及到针对某个个业务功能的调用,Controller会直接调用Model;如果呈现业务数据,Controller会通过Model获取相应业务数据并转换成View Model,最终通过View呈现出来。这样的交互协议方式反映了Controller针对Model的直接依赖。
如果我们在Controller激活系统中引入IoC,并采用IoC的方式提供用于处理请求的Controller对象,那么Controller和Model之间的依赖程度在很大程度上降低。我们甚至可以像下图所示的一样,以接口的方式都Model进行抽象,让Controller依赖于这个抽象化的Model接口,而不是具体的Model实现。
三、 创建基于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的路由系统:URL与物理文件的分离》中使用过的关于“员工管理”的场景,如下图所示,本实例由两个页面(对应着两个View)组成,一个用于显示员工列表,另一个用于显示基于某个员工的详细信息。
我们通过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>
ASP.NET MVC Controller激活系统详解:总体设计
ASP.NET MVC Controller激活系统详解:默认实现
ASP.NET MVC Controller激活系统详解:IoC的应用[上篇]
ASP.NET MVC Controller激活系统详解:IoC的应用[下篇]