【一看就懂】ASP.NET Core 所使用的 Dependency Injection

Dependency injection 並不是一個新東西,但對 ASP.NET Core 來說是一個新東西.這篇文章將說明 ASP.NET Core 如何使用 dependency injection 的概念.

時間已來到五月下旬了,現在各位 ASP.NET 的愛好者們已經可以下載最近釋出的 RC2 來試用 ASP.NET Core 最新的改進.RC2 的釋出也包含了 Visual Studio 工具鏈,所以大家可以使用 Visual Studio 來寫 ASP.NET Core,這對於不熟悉 dotnet.exe 與習慣用 Visual Studio 的使用者來說是相當方便的事情.接下來,這篇文章將分享有關 ASP.NET Core 如何應用 dependency injection 的概念在整個產品線裡.

基本概念

這文章並不會討論有關 dependency injection 的概念與細節,這部份的內容你可以在網路上找到很多很好的文章來參考.簡單的來說,dependency injection 是一種撰寫物件導向程式的寫法技巧,這個技巧能幫助你將讓物件之間減少相依度,進而使得你的程式變得比較容易修改,也較容易調整或新增功能.ASP.NET Core 也應用了這樣的概念在整個產品線中,從 runtime 到 mvc framework 都可以運用這樣的概念.說的白話一點,你可以在 ASP.NET Core 準備啟動的時候將你的物件新增到 dependency injection 所使用的儲存空間,然後在 MVC 的 controller 裡把這些物件找出來,進而呼叫物件上的函式.所以,透過這樣的方式,你的 controller 並不需要知道他需要 new 什麼元件,而是直接使用它就行了.前面提到 dependency injection 所使用的儲存空間其實是一個 List.你所要放進來的物件都會被包裝成 ServiceDescriptor class 而放在這個 List 當中,而 IServiceCollection interface 就繼承了這個 List.所以,當你在 Startup.cs 撰寫啟動時所需要的程式碼時,你就可以透過 IServiceCollection 把你所需要加入的物件新增到 dependency injection 的儲存空間中.然後在當你需要使用它時,也是透過 IServiceCollection 將物件取出就可以直接進行呼叫了.ASP.NET Core 所使用的 dependency injection 概念就是這麼簡單與直覺.

上面講的內容是有關如何將元件新增到儲存空間以及如何使用它.然而在儲存的生命周期有三種方式可以選擇,分別 Transient, Scope, Singleton,Transient 是指每次 http request 連線進來時,這個物件都會 re-instance,也就是說都會重新產生一個物件來服務這個 request.Scope 是指如果是同一個 client 連線過來的 request,都會使用到同一個物件,因此不同 client 的 request 所使用到的物件是不同的.Singleton 是指所有的 request 都會使用同一個物件,也就是說這個物件在 ASP.NET Core 啟動之後只會被 instance 一次而己.從這說明讓你可以了解到放到儲存空間有多少個物件並不是由你來執行的,一切都是透過 Microsoft.Extensions.DependencyInjection 元件來執行所有的動作.所以,你放到儲存空間也並不是元件,而只是元件的 type 或 Func delegate 而己,讓 Microsoft.Extensions.DependencyInjection 在運行時可以知道要把什麼 type 做 instance 的動作放在儲存空間中.

使用

請容許我做一個很笨的例子來說明如何使用. 我要做一個元件,這個元件可以幫我把訊息寫入到某個地方.所以,如果我想要寫入到檔案,我可以實作一個寫入內容到檔案的 class,如果我想寫到 email,我可以實作另一個產生 email 的 class,不論要我寫入到什麼地方,這些 class 都會繼承相同的 interface,所以就可以讓之後要使用該功能的人知道有什麼樣的 method 可以呼叫.因此,傳入的 class 不同,執行的功能就不同,但全部都遵守相同的 interface.這也就是 dependency injection 的基本精神,因此,先定義了一個 interface 如下:

    public interface ISampleService
    {
        void Write(string message);
    }

然後再定義一個 class 來實作這個 interface.

    public class SampleService : ISampleService
    {
        object _sync = new object();

        public void Write(string message)
        {
            lock (_sync)
            {
                using (System.IO.StreamWriter sw = new System.IO.StreamWriter("ThisIsAFile.txt", true))
                {
                    sw.WriteLine(message);
                }
            }
            
        }
    }

你可以看到 SampleService class 是一個實作把訊息寫入到檔案上的 class.

在 Startup.cs 中的 ConfigureServices 裡把這個 class 新增到 dependency injection 的儲存空間中.

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton<ISampleService, SampleService>();
        // 忽略其他程式碼

IServiceCollection 有幾個 extension method,其中一個叫 AddSingleton,也就是前面所說的,這個物件在被使用時只會被 instance 一次,所以所有的 client request 都會使用到同一個物件.如果需要其他的方式,也可使用 AddTransient 或 AddScoped 來新增物件.要用那一種方式完全看你所需要的情境是那一種.

接著在 MVC Framework 中來使用這些物件,在 Controller 的 constructor,可以將物件的定義帶入來取得該物件

    public class HomeController : Controller
    {
        ISampleService _sampleService;

        public HomeController(ISampleService service)
        {
            _sampleService = service;
        }

        public IActionResult Index()
        {
            _sampleService.Write("This is home controller Index action");
            return View();
        }
        // 忽略後面程式碼

透過上述的程式碼就知道如何使用它.接下來,你可能會問到你並沒有看到從 IServiceCollection 的 List 中把物件取出來的動作.對的,在這裡並沒有看到,那是因為 MVC Framework 幫你做了那些工作了.在 HomeController 的 constructor 被呼叫之前, MVC framework 執行了 Microsoft.AspNetCore.Mvc.Controllers.DefaultControllerFactory.CreateController (這是全名),CreateController 在執行時就會從 ControllerContext 那把 IServiceCollection 的內容取出來,而 ControllerContext 的內容你可以視為是 HttpContext 的子集合,ServiceCollection 就是透過 HttpContext 來傳遞的.然後當這個 controller 要建立的過程中 (DefaultControllerActivator.Create method) ,它發現它有輸入參數,這參數就是 dependency injection 儲存空間裡物件的 type,於是會到 ControllerContext 去尋找 dependency inject 的 List 儲存空間,然後把物件讀取出來.這一段的工作是 MVC Framework 幫你完成了,所以你並不會看到這一段內容,但它的確存在的.因此,如果 dependency injection 的儲存空間裡沒有 ISampleService 的話,就會發出 InvalidOperationException 用來說明找不到 ISampleService.

以上的 ASP.NET Core dependency injection 是用在 MVC 的情況中.如我前面的內容有提到,dependency injection 的儲存空間是透過 HttpContext 傳遞的,因此在 Middlware 中也是可以把 dependency injection 的物件讀取出來然後呼叫其中的函式.這部份我將在後面的文章中來說明.

原始碼

在這文章中我盡量少談到了原始碼,如果你對這些原始碼有興趣,你可以參考下面連結,這將列出一些 dependency injection 重要的原始碼.

https://github.com/aspnet/DependencyInjection/blob/master/src/Microsoft.Extensions.DependencyInjection.Abstractions/IServiceCollection.cs

https://github.com/aspnet/DependencyInjection/blob/master/src/Microsoft.Extensions.DependencyInjection.Abstractions/ServiceCollectionExtensions.cs

https://github.com/aspnet/DependencyInjection/blob/master/src/Microsoft.Extensions.DependencyInjection.Abstractions/ServiceDescriptor.cs

https://github.com/aspnet/DependencyInjection/blob/master/src/Microsoft.Extensions.DependencyInjection/ServiceCollection.cs

https://github.com/aspnet/Mvc/blob/master/src/Microsoft.AspNetCore.Mvc.Core/Controllers/DefaultControllerFactory.cs

https://github.com/aspnet/Mvc/blob/master/src/Microsoft.AspNetCore.Mvc.Core/Controllers/DefaultControllerActivator.cs

https://github.com/aspnet/Mvc/blob/master/src/Microsoft.AspNetCore.Mvc.Core/Internal/TypeActivatorCache.cs