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

  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實作請參考《ASP.NET MVC 4網站開發美學》第七章 Page 7-40頁。

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

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

  // 容器建立者
        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

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

    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的方式來搜尋與註冊。

Models結構

圖片來源: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年都有在更新。

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

    回覆刪除

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