使用Autofac和Unity學習MVC 4,5和Web API 1,2的IoC,DI技巧

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會是這樣:

01public class ProductsController : ApiController
02{
03    private NorthwindEntities db = new NorthwindEntities();
04 
05    // GET api/Products
06    public IEnumerable<Product> GetProducts()
07    {
08        return db.Products.AsEnumerable();
09    }
10 
11    // 省略
12}

預設範本所產生的程式碼會產生與NorthwindEntities資料實體(new NorthwindEntities())產生依賴關係。每個API Controller都還要處理db.Products,這造成API Controller的職責不夠單一,這在裡我們就先採用Repository Pattern 1來分離資料庫處理程式碼與API Controller的相依關係。

1 Repository Pattern實作請參考《ASP.NET MVC 4網站開發美學》第七章 Page 7-40頁。

01public class ProductsController : ApiController
02{
03    //private NorthwindEntities db = new NorthwindEntities();
04 
05    private IProductRepository _product;
06 
07    public ProductsController() {
08        _product = new ProductRepository();
09    }
10 
11    // 重要,但要等一下才會知道
12    public ProductsController(IProductRepository r)
13    {
14        _product = r;
15    }
16 
17    // GET api/Products
18    public IEnumerable<Product> GetProducts()
19    {
20        return _product.GetAll();
21    }
22 
23    // 省略
24}

透過Repository Patter我們分離了資料庫相關程式碼,而且我們偷偷留下一個含介面參數的建構函式,等一下在討論這部分。就目前使用了Repository Pattern的Controller,就算刪除此含介面參數的建構函式,程式依然會正常運作。

但引入使用Repository Pattern後的API Controller產生了新的問題,這些API Controller又會與特定實體類別產生相依關係,我們必須在無參數建構函式去初始化一個實體類別,以呼叫分離後的資料庫處理程式(例如:_product = new ProductRepository();)。

移除特定實體相依性

我們先看完成式,再來討論怎麼做。

01public class ProductsController : ApiController
02{
03    //private NorthwindEntities db = new NorthwindEntities();
04 
05    private IProductRepository _product;
06 
07    //public ProductsController()
08    //{
09    //    _product = new ProductRepository();
10    //}
11  
12           /// <summary>
13               /// 注入實體之用
14           /// </summary>
15           /// <param name="r">實作的介面</param>
16           public Products2Controller(IProductRepository r)
17           {
18               _product = r;
19                  }
20 
21    // GET api/Products
22    public IEnumerable<Product> GetProducts()
23    {
24        return _product.GetAll();
25    }
26 
27    // 省略
28}

最終希望可以把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都有快速與良好的支援。這是讓我選擇使用它的一個主因。

以下是《ASP.NET MVC 4網站開發美學》第七章 Page 7-46頁的程式碼:

01// 容器建立者
02      ContainerBuilder builder = new ContainerBuilder();
03 
04      // 註冊型別
05      builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
06             .Where(t => t.Name.EndsWith("Repository")).AsImplementedInterfaces();
07 
08      // 註冊服務(示範)
09      // builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly()) _
10      //     .Where(Function(t) t.Name.EndsWith("Service")) _
11      //     .AsImplementedInterfaces()
12 
13      builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
14             .Where(t => !t.IsAbstract & typeof(ApiController)
15             .IsAssignableFrom(t))
16             .InstancePerMatchingLifetimeScope(AutofacWebApiDependencyResolver.ApiRequestTag);
17 
18      // 建立容器
19      IContainer container = builder.Build();
20      // 解析容器內的型別
21      AutofacWebApiDependencyResolver resolver = new AutofacWebApiDependencyResolver(container);
22      // 註冊相依解析者
23      config.DependencyResolver = resolver;

以上是Autofac在Web API剛推出時所撰寫的程式碼。不過後來Autofac 3.x.x針對Web API方面有進行重構,以下是新版程式碼:

01using Autofac;
02using Autofac.Integration.WebApi;
03 
04// 容器建立者
05var builder = new ContainerBuilder();
06// 註冊Web API Controllers
07builder.RegisterApiControllers(Assembly.GetExecutingAssembly());
08// 註冊相依關係
09builder.Register(c => new ProductRepository()).As<IProductRepository>().InstancePerApiRequest();
10// 建立容器
11var container = builder.Build();
12// 建立相依解析器
13var resolver = new AutofacWebApiDependencyResolver(container);
14// 組態Web API相依解析器
15GlobalConfiguration.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

1WebApiConfig.SetAutofacDI(GlobalConfiguration.Configuration);

ASP.NET Web API 2

1GlobalConfiguration.Configure(WebApiConfig.SetAutofacDI);

Web API DI Framework - Unity Application Block

  • Install-Package unity.WebAPI

以下是《ASP.NET MVC 4網站開發美學》第七章 Page 7-48頁的程式碼:

01public static void Register(HttpConfiguration config)
02{
03 // 以上省略
04 var container = BuildUnityContainer();
05 GlobalConfiguration.Configuration.DependencyResolver = new Unity.WebApi.UnityDependencyResolver(container);
06}
07 
08private static IUnityContainer BuildUnityContainer()
09{
10    var container = new UnityContainer();
11 
12    // register all your components with the container here
13    // e.g. container.RegisterType<ITestService, TestService>();
14    container.RegisterType<IProductRepository, ProductRepository>();
15    return container;
16}

讀者如果實作做過一次,就會發現程式碼其實只有一行,將實體與介面的對應關係透過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程式碼:

01using Autofac;
02using Autofac.Integration.Mvc;
03 
04public static class AutofacConfig
05{
06    public static void Register()
07    {
08        // 容器建立者
09        var builder = new ContainerBuilder();
10        // 註冊Controllers
11        builder.RegisterControllers(Assembly.GetExecutingAssembly());
12        // 註冊型別
13        builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
14               .Where(t => t.Name.EndsWith("Repository"))
15               .AsImplementedInterfaces();
16        // 註冊服務(示範)
17        // builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
18        //        .Where(Function(t) t.Name.EndsWith("Service"))
19        //        .AsImplementedInterfaces()
20        // 建立容器
21        var container = builder.Build();
22        // 指定解析器
23        DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
24    }
25}

MVC 4的Autofac註冊程式碼和Web API其實差不多,透過builder.RegisterAssemblyTypes()方法去搜尋尾綴(Repository | Service)的檔案名稱,並進行註冊動作。

搜尋尾綴的方法,在大部分的專案情況下都可以符合需求了。不過還有一種註冊方法也不錯用,就是透過搜尋NameSpace的方式來搜尋與註冊。

Models結構

圖片來源:http://weblogs.asp.net/bsimser/

如果你的Models很複雜,可能會利用目錄來整理相關類別與實作。那麼我們只需要透過NameSpace來進行搜尋與註冊的動作即可:

01public static void Register()
02{
03    // 容器建立者
04    var builder = new ContainerBuilder();
05 
06    // 註冊Controllers
07    builder.RegisterControllers(Assembly.GetExecutingAssembly());
08 
09       // 註冊組件型別
10       // 這裡我們透過 Namespace 來取得所有介面與介面實作
11        builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
12                  .Where(t => t.Namespace.EndsWith(".Repositories"))
13                  .AsImplementedInterfaces();
14 
15    // 建立容器
16    var container = builder.Build();
17 
18    // 指定解析器
19    DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
20}  

透過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:

01public static IUnityContainer Initialise()
02{
03 var container = BuildUnityContainer();
04 DependencyResolver.SetResolver(new UnityDependencyResolver(container));
05 return container;
06}
07 
08private static IUnityContainer BuildUnityContainer()
09{
10 var container = new UnityContainer();
11 // register all your components with the container here
12 // it is NOT necessary to register your controllers
13 // e.g. container.RegisterType<ITestService, TestService>();
14 container.RegisterType<IProductRepository, ProductRepository>();
15 RegisterTypes(container);
16 return container;
17}
18 
19public static void RegisterTypes(IUnityContainer container)
20{
21}

我是習慣將組態檔整理至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年都有在更新。

7 則留言:

  1. 所以請問一下目前有針對web api2支援的嗎??我目前找了一堆,都顯示這種錯誤,無法載入檔案或組件 'System.Web.Http, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' 或其相依性的其中之一。 找到的組件資訊清單定義與組件參考不符。 (發生例外狀況於 HRESULT: 0x80131040)

    回覆刪除
    回覆
    1. Version=4.0.0.0,應該是 Web API 不是 Web API 2 哦!

      刪除
    2. 不好意思,我目前已經更新到最新版的web api2.1了,目前使用您文內介紹的Autofac,不過在您指到的這段code。
      ASP.NET Web API 2
      1 GlobalConfiguration.Configure(WebApiConfig.SetAutofacDI);
      SetAutofacDI解析不到,請問一下除了安裝autofac.WebApi2,還需要安裝什麼,或者是什麼原因呢??請指導小弟一下好嗎??非常的感謝。

      刪除
  2. WebApiConfig.SetAutofacDI ... 要自己寫呀。
    怎麼寫,本 Blog 旁邊有一本書,沒記錯的話,書裡面有教 :P

    回覆刪除
    回覆
    1. 你好,你說的書是指「asp.net mvc5網站開發美學」這本嗎?
      這本我手邊有一本,但是找不到相關的介紹耶,還是只在mvc4的那本才有介紹?
      謝謝

      刪除
  3. 你好,我是你的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這個方法的內容改成你這裡寫的
    新版本,結果還是會出現這個錯誤訊息,可否請你解答一下??謝謝

    回覆刪除

感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。