IoC與DI
- IoC
- Inversion of control,中文稱控制反轉。由Martin Fowler提出。這裡引用MVP 91哥的說明:
系統架構設計應該以抽象的邏輯概念為主,才能更貼近現實世界,才能更符合domain model,當Business logic不變時,需求變更、技術變更、DB變更,都應該要把風險和成本壓到最低。
- DI
- Dependency injection,中文稱相依注入。在IoC的思考模式與實作下,最終的結果就是相依於介面(Interface),但類別中最後只存有介面的話,程式是不會動的,這時我們就會利用DI技巧,在應用程式啟動時去注入介面所對應的實體(也就是一般我們在程式碼裡寫的
new ClassName()
)。
MVC與Web API的Controller的IoC與DI處理
不管是在ASP.NET MVC或ASP.NET Web API,在Controller的概念上或本質上可以說是一模一樣。這裡主要差異點會是在DI套件的實作差異上。
Controller使用Respository Pattern處理資料實體相依
一般我們會透過基架(MVC 5將scaffold翻譯為基架)來產生MVC的Controller與View Page,或是Web API的API Controller。例如,使用Northwind資料庫的Product資料表來產生API Controller會是這樣:
public class ProductsController : ApiController { private NorthwindEntities db = new NorthwindEntities(); // GET api/Products public IEnumerable<Product> GetProducts() { return db.Products.AsEnumerable(); } // 省略 }
預設範本所產生的程式碼會產生與NorthwindEntities資料實體(new NorthwindEntities())產生依賴關係。每個API Controller都還要處理db.Products,這造成API Controller的職責不夠單一,這在裡我們就先採用Repository Pattern 1來分離資料庫處理程式碼與API Controller的相依關係。
1 Repository Pattern實作請參考《
public class ProductsController : ApiController { //private NorthwindEntities db = new NorthwindEntities(); private IProductRepository _product; public ProductsController() { _product = new ProductRepository(); } // 重要,但要等一下才會知道 public ProductsController(IProductRepository r) { _product = r; } // GET api/Products public IEnumerable<Product> GetProducts() { return _product.GetAll(); } // 省略 }
透過Repository Patter我們分離了資料庫相關程式碼,而且我們偷偷留下一個含介面參數的建構函式,等一下在討論這部分。就目前使用了Repository Pattern的Controller,就算刪除此含介面參數的建構函式,程式依然會正常運作。
但引入使用Repository Pattern後的API Controller產生了新的問題,這些API Controller又會與特定實體類別產生相依關係,我們必須在無參數建構函式去初始化一個實體類別,以呼叫分離後的資料庫處理程式(例如:_product = new ProductRepository();
)。
移除特定實體相依性
我們先看完成式,再來討論怎麼做。
public class ProductsController : ApiController { //private NorthwindEntities db = new NorthwindEntities(); private IProductRepository _product; //public ProductsController() //{ // _product = new ProductRepository(); //} /// <summary> /// 注入實體之用 /// </summary> /// <param name="r">實作的介面</param> public Products2Controller(IProductRepository r) { _product = r; } // GET api/Products public IEnumerable<Product> GetProducts() { return _product.GetAll(); } // 省略 }
最終希望可以把Controller裡特定實體相依性給移除。然後透過IoC/DI套件的幫忙,讓我們可以透過Repository介面的幫助下去自動產生實體。
以下會介面二套IoC/DI套件,並討論在MVC與Web API的實作方式。
Web API DI Framework - Autofac
- Install-Package autofac.WebApi
Autofac算是老牌的IoC與DI實作套件,它對於Web API/Web API 2或是MVC 2/MVC 3/MVC 4/MVC 5都有快速與良好的支援。這是讓我選擇使用它的一個主因。
以下是《
// 容器建立者 ContainerBuilder builder = new ContainerBuilder(); // 註冊型別 builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) .Where(t => t.Name.EndsWith("Repository")).AsImplementedInterfaces(); // 註冊服務(示範) // builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) _ // .Where(Function(t) t.Name.EndsWith("Service")) _ // .AsImplementedInterfaces() builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) .Where(t => !t.IsAbstract & typeof(ApiController) .IsAssignableFrom(t)) .InstancePerMatchingLifetimeScope(AutofacWebApiDependencyResolver.ApiRequestTag); // 建立容器 IContainer container = builder.Build(); // 解析容器內的型別 AutofacWebApiDependencyResolver resolver = new AutofacWebApiDependencyResolver(container); // 註冊相依解析者 config.DependencyResolver = resolver;
以上是Autofac在Web API剛推出時所撰寫的程式碼。不過後來Autofac 3.x.x針對Web API方面有進行重構,以下是新版程式碼:
using Autofac; using Autofac.Integration.WebApi; // 容器建立者 var builder = new ContainerBuilder(); // 註冊Web API Controllers builder.RegisterApiControllers(Assembly.GetExecutingAssembly()); // 註冊相依關係 builder.Register(c => new ProductRepository()).As<IProductRepository>().InstancePerApiRequest(); // 建立容器 var container = builder.Build(); // 建立相依解析器 var resolver = new AutofacWebApiDependencyResolver(container); // 組態Web API相依解析器 GlobalConfiguration.Configuration.DependencyResolver = resolver;
透過builder.Register()
方法非常容易就註冊實體類別與介面的對應關係。現在API Controller已經可以在沒有任何實體,透過Autofac套件的幫忙正常運作。
2 以上兩段程式碼都能正常運作。
Web API 2 DI Framework - Autofac
- Install-Package autofac.WebApi2
程式碼完全相容,不過WebApiConfig.cs註冊有些差異:
ASP.NET Web API
WebApiConfig.SetAutofacDI(GlobalConfiguration.Configuration);
ASP.NET Web API 2
GlobalConfiguration.Configure(WebApiConfig.SetAutofacDI);
Web API DI Framework - Unity Application Block
- Install-Package unity.WebAPI
以下是《
public static void Register(HttpConfiguration config) { // 以上省略 var container = BuildUnityContainer(); GlobalConfiguration.Configuration.DependencyResolver = new Unity.WebApi.UnityDependencyResolver(container); } private static IUnityContainer BuildUnityContainer() { var container = new UnityContainer(); // register all your components with the container here // e.g. container.RegisterType<ITestService, TestService>(); container.RegisterType<IProductRepository, ProductRepository>(); return container; }
讀者如果實作做過一次,就會發現程式碼其實只有一行,將實體與介面的對應關係透過container.RegisterType()方法一一註冊進去即可。
3 可另外參考91哥的[ASP.NET Web API]3 分鐘搞定 DI framework–Unity Application Block
Web API 2 DI Framework - Unity Application Bloc
unity.WebAPI撰文期間(2013/10/29)未支援ASP.NET Web API 2。
ASP.NET MVC 4 DI Framework - Autofac
MVC專案先透過Repository Pattern來整理Controller裡的資料庫存取程式碼。然後使用NuGet安裝必需要的元件:
- Install-Package autofac.Mvc4
autofac.Mvc4會自動加入相依的組件。我們盡量保持 Global.asax 檔的乾淨,新增一個AutofacConfig.cs來整理我們的DI Framework程式碼:
using Autofac; using Autofac.Integration.Mvc; public static class AutofacConfig { public static void Register() { // 容器建立者 var builder = new ContainerBuilder(); // 註冊Controllers builder.RegisterControllers(Assembly.GetExecutingAssembly()); // 註冊型別 builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) .Where(t => t.Name.EndsWith("Repository")) .AsImplementedInterfaces(); // 註冊服務(示範) // builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) // .Where(Function(t) t.Name.EndsWith("Service")) // .AsImplementedInterfaces() // 建立容器 var container = builder.Build(); // 指定解析器 DependencyResolver.SetResolver(new AutofacDependencyResolver(container)); } }
MVC 4的Autofac註冊程式碼和Web API其實差不多,透過builder.RegisterAssemblyTypes()
方法去搜尋尾綴(Repository | Service)的檔案名稱,並進行註冊動作。
搜尋尾綴的方法,在大部分的專案情況下都可以符合需求了。不過還有一種註冊方法也不錯用,就是透過搜尋NameSpace的方式來搜尋與註冊。
圖片來源:http://weblogs.asp.net/bsimser/
如果你的Models很複雜,可能會利用目錄來整理相關類別與實作。那麼我們只需要透過NameSpace來進行搜尋與註冊的動作即可:
public static void Register() { // 容器建立者 var builder = new ContainerBuilder(); // 註冊Controllers builder.RegisterControllers(Assembly.GetExecutingAssembly()); // 註冊組件型別 // 這裡我們透過 Namespace 來取得所有介面與介面實作 builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) .Where(t => t.Namespace.EndsWith(".Repositories")) .AsImplementedInterfaces(); // 建立容器 var container = builder.Build(); // 指定解析器 DependencyResolver.SetResolver(new AutofacDependencyResolver(container)); }
透過Lambda運算式「t.Namespace.EndsWith(".Repositories")
」找出此NameSpace下所有介面與實作並進行註冊動作。
ASP.NET MVC 5 DI Framework - Autofac
- Install-Package autofac.Mvc5
ASP.NET MVC 5必須用使用autofac.Mvc5套件版本。
實作程式碼與ASP.NET MVC 4一模一樣。互相通用。
ASP.NET MVC 4 DI Framework - Unity Application Block
- Install-Package unity.Mvc4
這邊讓我偷懶一下,我直修改安裝好的Bootstrapper.cs:
public static IUnityContainer Initialise() { var container = BuildUnityContainer(); DependencyResolver.SetResolver(new UnityDependencyResolver(container)); return container; } private static IUnityContainer BuildUnityContainer() { var container = new UnityContainer(); // register all your components with the container here // it is NOT necessary to register your controllers // e.g. container.RegisterType<ITestService, TestService>(); container.RegisterType<IProductRepository, ProductRepository>(); RegisterTypes(container); return container; } public static void RegisterTypes(IUnityContainer container) { }
我是習慣將組態檔整理至App_Start目錄,這樣比較統一。新增介面與實體的對應關係只有一行程式碼:「container.RegisterType<IProductRepository, ProductRepository>();
」,使用方式和Web API的Unity.WebApi一樣簡便。
ASP.NET MVC 5 DI Framework - Unity Application Block
- Install-Package unity.Mvc4
目前在ASP.NET MVC 5使用與設置並無差異,測試unity.Mvc4組件在MVC 5專案可以正常作運。
IoC / DI套件執行效率
目前世面上的IoC / DI套件很多,有位Daniel比較受歡迎的IoC / DI套件的執行效率。如果看到文末的Updates,就知道這篇文章有非常不錯的參考價值,因為套件會不算改版升級,作者從2011年一直到2013年都有在更新。
所以請問一下目前有針對web api2支援的嗎??我目前找了一堆,都顯示這種錯誤,無法載入檔案或組件 'System.Web.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' 或其相依性的其中之一。 找到的組件資訊清單定義與組件參考不符。 (發生例外狀況於 HRESULT: 0x80131040)
回覆刪除Version=4.0.0.0,應該是 Web API 不是 Web API 2 哦!
刪除不好意思,我目前已經更新到最新版的web api2.1了,目前使用您文內介紹的Autofac,不過在您指到的這段code。
刪除ASP.NET Web API 2
1 GlobalConfiguration.Configure(WebApiConfig.SetAutofacDI);
SetAutofacDI解析不到,請問一下除了安裝autofac.WebApi2,還需要安裝什麼,或者是什麼原因呢??請指導小弟一下好嗎??非常的感謝。
WebApiConfig.SetAutofacDI ... 要自己寫呀。
回覆刪除怎麼寫,本 Blog 旁邊有一本書,沒記錯的話,書裡面有教 :P
你好,你說的書是指「asp.net mvc5網站開發美學」這本嗎?
刪除這本我手邊有一本,但是找不到相關的介紹耶,還是只在mvc4的那本才有介紹?
謝謝
MVC 4
刪除你好,我是你的asp.net mvc 4 網站開發美學的讀者,我在實作web api的章節時,做到相依注入的部分在使用autofac這個套件時發生問題
回覆刪除照書上的步驟做下來,在瀏覽器輸入http://http://localhost:62284/api/products/時會出現
An error has occurred.
No constructors on type 'MvcApplication2.Controllers.APIs.ProductsController' can be found with the constructor finder 'Autofac.Core.Activators.Reflection.DefaultConstructorFinder'.
Autofac.Core.DependencyResolutionException
於 Autofac.Core.Activators.Reflection.ReflectionActivator.ActivateInstance(IComponentContext context, IEnumerable`1 parameters) 於 Autofac.Core.Resolving.InstanceLookup.Activate(IEnumerable`1 parameters) 於 Autofac.Core.Resolving.InstanceLookup.Execute() 於 Autofac.Core.Resolving.ResolveOperation.GetOrCreateInstance(ISharingLifetimeScope currentOperationScope, IComponentRegistration registration, IEnumerable`1 parameters) 於 Autofac.Core.Resolving.ResolveOperation.ResolveComponent(IComponentRegistration registration, IEnumerable`1 parameters) 於 Autofac.Core.Resolving.ResolveOperation.Execute(IComponentRegistration registration, IEnumerable`1 parameters) 於 Autofac.Core.Lifetime.LifetimeScope.ResolveComponent(IComponentRegistration registration, IEnumerable`1 parameters) 於 Autofac.ResolutionExtensions.TryResolveService(IComponentContext context, Service service, IEnumerable`1 parameters, Object& instance) 於 Autofac.ResolutionExtensions.ResolveOptionalService(IComponentContext context, Service service, IEnumerable`1 parameters) 於 Autofac.ResolutionExtensions.ResolveOptional(IComponentContext context, Type serviceType, IEnumerable`1 parameters) 於 Autofac.ResolutionExtensions.ResolveOptional(IComponentContext context, Type serviceType) 於 Autofac.Integration.WebApi.AutofacWebApiDependencyScope.GetService(Type serviceType) 於 System.Web.Http.Dispatcher.DefaultHttpControllerActivator.GetInstanceOrActivator(HttpRequestMessage request, Type controllerType, Func`1& activator) 於 System.Web.Http.Dispatcher.DefaultHttpControllerActivator.Create(HttpRequestMessage request, HttpControllerDescriptor controllerDescriptor, Type controllerType)
這個錯誤訊息~~原本以為我下載的版本較新(autofac.webapi 3.1.0版)將SetAutofacDI這個方法的內容改成你這裡寫的
新版本,結果還是會出現這個錯誤訊息,可否請你解答一下??謝謝