所謂控制反轉(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的應用[下篇]