2018年11月17日 星期六

ASP.NET Core IoC 容器與整合 Unity 容器之相依性注入之應用

ASP.NET Core IoC 容器與整合 Unity 容器之相依性注入之應用

ASP.NET Core 的專案將會大量使用到相依性注入 Dependency Injection 這個設計模式 Design Pattern,也就是說,若身為開發者的你對於物件導向的控制反轉設計 Inversion of Control (IoC)、SOLID原則的相依反轉原則 Dependency Inversion Principle (DIP) 、相依性注入 Dependency Injection、IoC 容器 Container、各種注入實作物件的方式並不瞭解,那麼,則是很不幸的,對於進行 ASP.NET Core 專案開發的時候,將會無法充分發揮各種好用的功能,因此,在本篇文章中,將會對於如何使用 ASP.NET Core 提供的內建 IoC 容器的用法進行使用方式說明,並且,透過 Unity 這個擴充的 DI Container / IoC Container 相依性注入容器是要如何安裝、設定與在 ASP.NET Core 專案上來使用,都將會透過實際開發專案程式碼進行說明。本篇文章的範例原始碼,可以從 ASPNETCoreIoCDI 取得。

建立測試專案

首先,先來將這個範例專案建立出來,並且看到這個專案的執行結果,稍後將會對於這些操作過程進行解說。
  • 啟動 Visual Studio 2017
  • 點選功能表的 [檔案] > [新增] > [專案] 功能表選項
  • 在 [新增專案] 對話窗中左邊區域,選擇 [已安裝] > [Visual C#] > [Web] > [ASP.NET Core Web 應用程式]
  • 在對話窗的下方的 [名稱] 欄位中,輸入這個練習專案的名稱 ASPNETCoreIoCDI
  • 在對話窗的下方的 [位置] 欄位中,選擇這個專案要儲存的檔案路徑
  • 最後點選對話窗的右下方 [確定] 按鈕
  • 在 [新增 ASP.NET Core Web 應用程式] 對話窗左上方區域,在第一個下拉選單,選擇 [.NET Core] 在第二個下拉選單,請選擇最新的 ASP.NET Core 版本,在現在這個時間點,可以選擇 [ASP.NET Core 2.1]
  • 在對話窗中間區域,請點選 [API] 這個項目
  • 右下方的 [驗證] 欄位,請確認為 [無驗證]
  • 最後點選對話窗的右下方 [確定] 按鈕
  • 在專案建立完成之後,滑鼠右擊專案節點,選擇 [管理 NuGet 套件] 選項
  • 在 [NuGet: ASPNETCoreIoCDI] 視窗,點選 [瀏覽] 標籤頁次,在搜尋文字輸入盒,輸入[Unity.Microsoft.DependencyInjection] ,當搜尋到這個 NuGet 套件之後,點選該項目,接著點選 [安裝] 按鈕,將這個套件安裝到這個專案內。
  • 滑鼠右擊專案節點,選擇 [加入] > [類別] 選項
  • 在 [新增項目] 對話窗中間區域內,點選 [介面],在對話窗下方名稱欄位,輸入 [IMyDependency] ,最後點選 [新增] 按鈕
C Sharp / C#
public interface IMyDependency
{
    Guid MyProperty { get; set; }
    string InstanceFrom { get; set; }
    string InstanceName { get; set; }
}
public class MyClass1 : IMyDependency
{
    Guid myProperty = Guid.NewGuid();
    public Guid MyProperty
    {
        get
        {
            return myProperty;
        }
        set
        {
            myProperty = value;
        }
    }
    public string ClassName { get; set; } = "MyClass1";
    public string InstanceFrom { get; set; } = "";
    public string InstanceName { get; set; } = "";
}
public class MyClass2 : IMyDependency
{
    Guid myProperty = Guid.NewGuid();
    public Guid MyProperty
    {
        get
        {
            return myProperty;
        }
        set
        {
            myProperty = value;
        }
    }
    public string ClassName { get; set; } = "MyClass2";
    public string InstanceFrom { get; set; } = "";
    public string InstanceName { get; set; } = "";
}
  • 在 [新增項目] 對話窗中間區域內,點選 [介面],在對話窗下方名稱欄位,輸入 [IDefaultDependency] ,最後點選 [新增] 按鈕
C Sharp / C#
public interface IDefaultDependency
{
    Guid MyProperty { get; set; }
    string InstanceFrom { get; set; }
    string InstanceName { get; set; }
}
public class MyDefaultClass1 : IDefaultDependency
{
    Guid myProperty = Guid.NewGuid();
    public Guid MyProperty
    {
        get
        {
            return myProperty;
        }
        set
        {
            myProperty = value;
        }
    }
    public string ClassName { get; set; } = "MyDefaultClass1";
    public string InstanceFrom { get; set; } = "";
    public string InstanceName { get; set; } = "";
}
  • 打開 [Startup.cs] 檔案,使用底下程式碼來建立一個 Container 屬性
C Sharp / C#
public static IUnityContainer Container { get; set; }
  • 打開 [Startup.cs] 檔案內,使用底下程式碼來新增 ConfigureContainer 方法
C Sharp / C#
public IServiceProvider ConfigureServicesIServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
    services.AddScoped<IDefaultDependency, MyDefaultClass1>();
    services.AddTransient<IMyDependency, MyClass2>();
    Unity.Microsoft.DependencyInjection.ServiceProvider serviceProvider =
        Unity.Microsoft.DependencyInjection.ServiceProvider.ConfigureServices(services)
        as Unity.Microsoft.DependencyInjection.ServiceProvider;
    ConfigureContainer(Container=(UnityContainer)serviceProvider);
    return serviceProvider;
}
  • 打開 [Startup.cs] 檔案,找到 ConfigureServices 方法,將這個方法修改成為底下程式碼
C Sharp / C#
public void ConfigureContainer(IUnityContainer ontainer)
{
    // Could be used to register more types
    container.RegisterType<IMyDependency, MyClass1>("MyClass1");
}
  • 在 [Controllers] 資料夾內,找到並且打開 [ValuesController.cs] 檔案,將 ValuesController 這個類別,使用底下程式碼來替換。
C Sharp / C#
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
    private readonly IMyDependency myDependency1;
    private readonly IMyDependency myDependency2;
    private readonly IServiceProvider serviceProvider;
    private readonly IUnityContainer container;
    private readonly IDefaultDependency defaultDependency1;
    private readonly IDefaultDependency defaultDependency2;
    public ValuesController(IMyDependency myDependency1, IMyDependency myDependency2,
        IServiceProvider serviceProvider, IUnityContainer container,
        IDefaultDependency defaultDependency1, IDefaultDependency defaultDependency2)
    {
        this.myDependency1 = myDependency1;
        this.myDependency1.InstanceFrom = "控制器的建構式注入";
        this.myDependency1.InstanceName = "myDependency1";
        this.myDependency2 = myDependency2;
        this.myDependency2.InstanceFrom = "控制器的建構式注入";
        this.myDependency2.InstanceName = "myDependency2";
        this.serviceProvider = serviceProvider;
        this.container = container;
        this.defaultDependency1 = defaultDependency1;
        this.defaultDependency1.InstanceFrom = "控制器的建構式注入";
        this.myDependency1.InstanceName = "myDependency1";
        this.defaultDependency2 = defaultDependency2;
        this.defaultDependency2.InstanceFrom = "控制器的建構式注入";
        this.defaultDependency2.InstanceName = "defaultDependency2";
    }
    // GET api/values
    [HttpGet]
    public ActionResult<IEnumerable<object>> Get()
    {
        IServiceProvider contextServiceProvider = HttpContext.RequestServices;
        IMyDependency myDependency3 = contextServiceProvider.GetService<IMyDependency>();
        myDependency3.InstanceFrom = "由 HttpContext 取得 IServiceProvider 進而手動解析";
        myDependency3.InstanceName = "myDependency3";
        IMyDependency myDependency4 = serviceProvider.GetService<IMyDependency>();
        myDependency4.InstanceFrom = "由 建構式注入 取得 IServiceProvider 進而手動解析";
        myDependency4.InstanceName = "myDependency4";
        IMyDependency myDependency5 = container.Resolve<IMyDependency>();
        myDependency5.InstanceFrom = "由 建構式注入 取得 IUnityContainer 進而手動解析(預設匿名解析)";
        myDependency5.InstanceName = "myDependency5";
        IMyDependency myDependency6 = container.Resolve<IMyDependency>("MyClass1");
        myDependency6.InstanceFrom = "由 建構式注入 取得 IUnityContainer 進而手動解析(具名解析,指定 MyClass1)";
        myDependency6.InstanceName = "myDependency6";
        IMyDependency myDependency7 = container.Resolve<IMyDependency>();
        myDependency7.InstanceFrom = "由 Startup 靜態屬性 Container 取得 IUnityContainer 進而手動解析(預設匿名解析)";
        myDependency7.InstanceName = "myDependency7";
        IMyDependency myDependency8 = Startup.Container.Resolve<IMyDependency>("MyClass1");
        myDependency8.InstanceFrom = "由 Startup 靜態屬性 Container 取得 IUnityContainer 進而手動解析(具名解析,指定 MyClass1)";
        myDependency8.InstanceName = "myDependency8";
        return new object[] { myDependency1, myDependency2, myDependency3, myDependency4,
        myDependency5, myDependency6, myDependency7, myDependency8,defaultDependency1,defaultDependency2};
    }
    // GET api/values/5
    [HttpGet("{id}")]
    public ActionResult<string> Get(int id)
    {
        return "value";
    }
    // POST api/values
    [HttpPost]
    public void Post([FromBody] string value)
    {
    }
    // PUT api/values/5
    [HttpPut("{id}")]
    public void Put(int id, [FromBody] string value)
    {
    }
    // DELETE api/values/5
    [HttpDelete("{id}")]
    public void Delete(int id)
    {
    }
}
  • 執行這個專案,將會看到網頁會有底下輸出結果
Console
[
  {
    "myProperty": "014ef344-2619-44b7-8a7b-c94845e4c978",
    "className": "MyClass2",
    "instanceFrom": "控制器的建構式注入",
    "instanceName": "myDependency1"
  },
  {
    myProperty: "408b4b5e-cca7-48cc-b7dc-c15444d143e7",
    className: "MyClass2",
    instanceFrom: "控制器的建構式注入",
    instanceName: "myDependency2"
  },
  {
    myProperty: "cfaf1568-591d-4a66-aca1-f51f461bbf05",
    className: "MyClass2",
    instanceFrom: "由 HttpContext 取得 IServiceProvider 進而手動解析",
    instanceName: "myDependency3"
  },
  {
    myProperty: "89453a9e-e322-48ae-8442-0804792e0bf7",
    className: "MyClass2",
    instanceFrom: "由 建構式注入 取得 IServiceProvider 進而手動解析",
    instanceName: "myDependency4"
  },
  {
    myProperty: "757edf1d-50e9-4ce5-b1f1-1836981b3bf0",
    className: "MyClass2",
    instanceFrom: "由 建構式注入 取得 IUnityContainer 進而手動解析(預設匿名解析)",
    instanceName: "myDependency5"
  },
  {
    myProperty: "bdc09609-8a2b-48ac-909b-ff85a74a50fd",
    className: "MyClass1",
    instanceFrom: "由 建構式注入 取得 IUnityContainer 進而手動解析(具名解析,指定 MyClass1)",
    instanceName: "myDependency6"
  },
  {
    myProperty: "15b124d1-3d88-432f-bb1c-fe6a1ccb253e",
    className: "MyClass2",
    instanceFrom: "由 Startup 靜態屬性 Container 取得 IUnityContainer 進而手動解析(預設匿名解析)",
    instanceName: "myDependency7"
  },
  {
    myProperty: "8567b2c6-b71f-4259-a536-846ff47f3809",
    className: "MyClass1",
    instanceFrom: "由 Startup 靜態屬性 Container 取得 IUnityContainer 進而手動解析(具名解析,指定 MyClass1)",
    instanceName: "myDependency8"
  },
  {
    myProperty: "7f9e8bb8-a891-471f-b06c-f378eefe49ab",
    className: "MyDefaultClass1",
    instanceFrom: "控制器的建構式注入",
    instanceName: "defaultDependency2"
  },
  {
    myProperty: "7f9e8bb8-a891-471f-b06c-f378eefe49ab",
    className: "MyDefaultClass1",
    instanceFrom: "控制器的建構式注入",
    instanceName: "defaultDependency2"
  }
]

物件導向的 Inversion of Control (IoC) 控制反轉

在要了解如何在 ASP.NET Core 專案內使用相依性注入的用法,對於專案內要使用的許多類別,為了降底這些類別的耦合力,因此,需要採用物件導向 Object-oriented programming (OOP) 的控制反轉的技術來進行這些類別的設計;也就是說,為了要把這些相依的類別間做到低耦合力關係,需要針對這些類別需求,進行資訊抽象化的設計,也就是在整個專案設計的程式碼內,都會針對抽象型別來設計專案程式碼,這使得在進行專案開發的時候,因為都是針對這些抽象型別來進行程式碼開發,所以,也就會與真正的具體實作類別沒有密切的關係,也就是說,到時候是由哪個具體實作類別來進行產生出執行個體 Instance 物件,並不會在設計類別的時候來進行考量與產生出這些執行個體,當然,在這個類別內應該也不會看到了 new SomeClass() 這樣用產生一個執行個體的表示式出現了。
不過,身為開發者也都知道,在 C# 程式語言中的抽象型別可以宣告為 abstract 的抽象類別或者是宣告為 interface 的介面,當一個型別宣告為 abstract 或者 interface 的時候,是不能夠透過 new 運算子來產生出執行出該型別的執行個體的,那麼,要如得到這些抽象型別的執行個體呢?這就需要透過相依性注入的設計模式。
在這個 ASP.NET Core 專案內的 ValuesController 控制器類別內,將會用到 MyClass1 與 MyDefaultClass1 這兩個類別所產生的執行個體,所以,第一個步驟就是要將這兩個類別進行資訊抽象化,對於 MyClass1 這個類別,將會設計出底下 介面 Interface 。在這個介面宣告了三個 屬性 Property,對於 MyProperty 這個屬性是屬於 Guid 型別,將會用來檢視這個由具體實作類別所產生的物件是否只會產生一次或者在每次都會產生一個新的執行個體;對於 InstanceFrom 這個屬性是屬於字串 string 型別,會用來說明產生的執行個體是由哪個具體實作類別所產生的;對於最後一個 InstanceName 這個屬性也是屬於字串型別,將會標示出這個執行個體的 C# 變數名稱。
C Sharp / C#
public interface IMyDependency
{
    Guid MyProperty { get; set; }
    string InstanceFrom { get; set; }
    string InstanceName { get; set; }
}
當設計出介面型別之後,接下來就需要使用這個介面來實作出類別,在這個專案內,使用這個語法 public class MyClass1 : IMyDependency 定義一個 MyClass1 這個類別,並且實作 IMyDependency 這介面;從底下的 MyClass1 定義程式碼,對於 MyProperty 這個類別,將會使用 含有支援欄位的屬性 方法來進行定義,在這個支援欄位會使用 Guid.NewGuid() 產生預設值,也就是說,當系統要使用 MyClass1 類別產生出多個執行個體的時候,對於 MyProperty 屬性值將會有不同的內容,而若發現多個屬於 MyClass1 的 C# 變數,他們的 MyProperty 的值都是相同的,則表示這些變數都是指向同一個 MyClass1 的執行個體。
C Sharp / C#
public class MyClass1 : IMyDependency
{
    Guid myProperty = Guid.NewGuid();
    public Guid MyProperty
    {
        get
        {
            return myProperty;
        }
        set
        {
            myProperty = value;
        }
    }
    public string ClassName { get; set; } = "MyClass1";
    public string InstanceFrom { get; set; } = "";
    public string InstanceName { get; set; } = "";
}
另外,在這裡也要說明當在 ASP.NET Core 專案中,需要用到多個具體實作類別都實作同一個介面情境,而當要注入這個介面的時候,究竟會產生出哪個具體實作的類別來產生出最終執行個體,因此,將會定義出 MyClass2 這個類別,也是實作出同一個 IMyDependency 這個介面,不過,對於 MyClass2 這個類別的 ClassName 屬性,將會預設指定為 MyClass2 這個字串名稱,這樣,我們就可以透過這個屬性值來了解到這個物件是由哪個具體實作類別所生成的,當然,也可透過當時的執行個體來呼叫 GetType() 方法,取得具體實作的類別名稱。
C Sharp / C#
public class MyClass2 : IMyDependency
{
    Guid myProperty = Guid.NewGuid();
    public Guid MyProperty
    {
        get
        {
            return myProperty;
        }
        set
        {
            myProperty = value;
        }
    }
    public string ClassName { get; set; } = "MyClass2";
    public string InstanceFrom { get; set; } = "";
    public string InstanceName { get; set; } = "";
}
最後,要能夠了解相依性注入這個設計模式中相當重要的一個設計技術,那就是生命週期管理,在這個專案將會設計一個 MyDefaultClass1 類別,將會實作 IDefaultDependency 介面,而對於 MyDefaultClass1 類別與IDefaultDependency 介面的設計內容將都會與前面討論到的 MyClass1 類別與 IMyDependency 介面相同。

預設 .NET Core 的 IoC 容器用法

當把專案內用到的各個類別,進行資訊抽象化作業,設計出相關的抽象型別,這裡使用的是 C# 介面 Interface 型別,並且這些類別都有實作出這些介面,接下來的工作將會需要使用 ASP.NET Core 專案中預設提供的 IoC 容器 Container (也可以稱做 相依性注入容器 DI Container,這兩個名詞代表了同一件事情),進行相依性注入中最為重要的 註冊 Registration 動作。通常來說,需要在專案的 組合根 (Composition Root) 地方來進行這個型別對應註冊的工作,通常來說,組合根 (Composition Root) 一般都是很接近於程式進入點 Entry Point 的附近。
在 ASP.NET Core 專案內,可以透過這個 Startup.cs 檔案內的 Startup 類別來進行抽象型別與具體實作型別的註冊工作,另外,在進行相依性注入要用到的型別註冊工作時候,也需要一併指定當要生成這些抽象型別之具體實作類別的執行個體的時候,是要每次要求注入同一個介面物件的時候,都要自動生成一個具體執行個體、還是在整個應用程式都在執行的工作成,每次注入同一個介面的時候,都只會得到同一個執行個體、又或者是當在同一個 HTTP 要求 Request 動作產生的時候,對於相同介面要注入多個物件的時候,都會只產生一個執行個體物件,不過,對於另外一次 HTTP 要求產生的時候,將會對於相同介面要注入多個物件的時候,又會只產生一個執行個體物件

註冊抽象型別與具體實作型別

想要進行型別的註冊動作,請打開 Startup.cs 檔案,找到 ConfigureServices(IServiceCollection services) 這個方法,在這個 ConfigureServices 方法內,將會有個 IServiceCollection services 參數,此時,可以透過這個參數 servers 使用底下的三個方法來進行抽象型別與具體實作型別的註冊。
  • IServiceCollection.AddTransient
    使用這個方法來進行抽象型別與具體實作型別註冊的動作,稱為暫時性 Transient,也就是說,當透過 IoC 容器注入一個該抽象型別的執行個體時候,都會實際產生與建立出這個執行個體。
  • IServiceCollection.AddScoped
    使用這個方法來進行抽象型別與具體實作型別註冊的動作,稱為具範圍 Scoped,也就是說,當透過 IoC 容器注入一個該抽象型別的執行個體時候,只要這些注入行為發生在同一個 HTTP 要求過程中,對應的具體實作類別所產生的執行個體只會產生一次而且只對在第一次要求注入該抽象型別的時候來建立該執行個體,也就是說,同一個 HTTP 要求中,若有多次注入同一個抽象型別的動作,得到的執行個體都是同一個;但是,當另外一次 HTTP 要求發生的時候,又會產生一個新的執行個體。
  • IServiceCollection.AddSingleton
    使用這個方法來進行抽象型別與具體實作型別註冊的動作,稱為單一 Singleton,也就是說,當透過 IoC 容器注入一個該抽象型別的執行個體時候,不論是在哪個 HTTP 要求過程中,只有在該應用程式執行後,第一次要求注入的時候,會建立該具體實作類別的執行個體,之後要注入該抽象型別的時候,就都會取回剛剛建立的同一個執行個體。
在這個範例專案中,可以從 ConfigureServices(IServiceCollection services) 方法內,看到底下兩行陳述式,第一行陳述式將會進行介面 IDefaultDependency 與 類別 MyDefaultClass1 的對應註冊,也就是說,當 IoC 容器被要求注入一個 IDefaultDependency 介面物件的時候,將會回傳一個 MyDefaultClass1 具體實作類別的執行個體,此處因為使用了 AddScoped 方法來進行註冊。
C Sharp / C#
services.AddScoped<IDefaultDependency, MyDefaultClass1>();
services.AddTransient<IMyDependency, MyClass2>();
因此,只要在同一個 HTTP 要求過程中,都只會回傳同一個 MyDefaultClass1 的執行個體,因此,當執行這個範例專案的時候,將會發現到對於屬性名稱為 className 的屬性值為 MyDefaultClass1 名稱的兩個物件,也就是在 ValuesController 類別中透過建構式注入的兩個欄位 defaultDependency1, defaultDependency2,這兩個欄位都透過建構式注入得到同一個物件,這可以從這兩個欄位的 myProperty 屬性值都是同一個 Guid 值來判斷出來。
Console
  {
    myProperty: "7f9e8bb8-a891-471f-b06c-f378eefe49ab",
    className: "MyDefaultClass1",
    instanceFrom: "控制器的建構式注入",
    instanceName: "defaultDependency2"
  },
  {
    myProperty: "7f9e8bb8-a891-471f-b06c-f378eefe49ab",
    className: "MyDefaultClass1",
    instanceFrom: "控制器的建構式注入",
    instanceName: "defaultDependency2"
  }
請將 services.AddScoped<IDefaultDependency, MyDefaultClass1>(); 陳述式修改為 services.AddTransient<IDefaultDependency, MyDefaultClass1>();,並且再度執行一次這個專案,查看屬性名稱為 className 的屬性值為 MyDefaultClass1 名稱的兩個物件輸出結果將會如下,他們的 myProperty 的 Guid 屬性值都是不相同,因此,可以判對出當使用 AddTrasient 這個方法來進行抽象型別與具體型別註冊後,透過 IoC 容器來注入這個抽象介面的具體實作物件的時候,每個要求注入需求動作都會實際建立起一個新的執行個體,這與上面使用 AddScoped 進行註冊的結果會有所不同的。
Console
  {
    myProperty: "8eead000-1f76-4d0a-b543-0ad6627e0394",
    className: "MyDefaultClass1",
    instanceFrom: "控制器的建構式注入",
    instanceName: "defaultDependency1"
  },
  {
    myProperty: "5c034da7-c20b-48e2-bbfd-ff50d84f662c",
    className: "MyDefaultClass1",
    instanceFrom: "控制器的建構式注入",
    instanceName: "defaultDependency2"
  }
還有,對於介面 IMyDependency 與具體實作類別 MyClass2 ,將會使用這個陳述式來進行註冊,services.AddTransient<IMyDependency, MyClass2>();,也就是當要透過 IoC 容器進行注入 IMyDependency 介面的時候,每次都會建立一個新的具體實作類別執行個體出來。
最後,當要進行註冊到 IoC 容器內的時候,也可以只指定具體類別,例如:services.AddTransient<MyClass2>();,這表示當透過 IoC 容器注入一個 MyClass2 類別的時候,IoC 容器可以自動建立一個這樣的執行個體出來。

使用建構式注入來注入實作物件

當需要透過 IoC 容器進行注入具體實作物件的時候,一般來說,可以透過 建構式注入、屬性注入、方法注入這三種方式來取得取體實作物件,不過,在 ASP.NET Core 內,僅支援 建構式注入 這種方式來注入取得具體實作的執行個體;但是,若想要使用另外兩種注入方式,可以透過其他的 IoC 容器來完成,在這裡將會使用 Unity 這個 DI 容器來做到這樣的需求。
當要使用的類別,其是透過 ASP.NET Core 開發框架來產生出一個執行個體,例如:繼承 ControllerBase 的相關控制器類別,而不是自己使用 new 運算子自己建立出來的執行個體,可以在該類別內的建構函式,將要注入介面放到該建構函式內參數內,如同底下的程式碼。在這裡,將會要透過 IoC 容器自動注入 IMyDependency, IMyDependency, IServiceProvider, IUnityContainer, IDefaultDependency, IDefaultDependency 這些物件作為該建構函式的參數。
因為所有要透過 IoC 容器來自動注入的物件,都需要事先在 組合根 (Composition Root) 地方進行註冊,因為,在這裡 IoC 容器將會知道 IMyDependency, IMyDependency, IDefaultDependency 的具體時最類別是哪個,不過,在前面的 ConfigureServices(IServiceCollection services) 方法內,似乎沒有進行 IServiceProvider, IUnityContainer 這兩個抽象介面的註冊,那麼,在這裡要注入甚麼具體實作的執行個體呢?
若在建構函式內所引用的參數,其抽象介面並沒有在 IoC 容器中進行註冊,則當透過 IoC 容器要注入這個介面時候,就會拋出例外異常錯誤;不過對於 IServiceProvider, IUnityContainer 這兩個抽象介面,前者是 ASP.NET Core 開發框架自動註冊進去的,後者則是安裝了 Unity 這個 DI 容器套件 (Unity.Microsoft.DependencyInjection) 自動註冊進去的,因此,可以放心在建構是的參數內直接使用。
在這個建構函式參數中,將會使用到兩次 IDefaultDependency 這個介面,在這裡是要用來檢測相依性注入的物件生命週期管理使用;若當註冊 IDefaultDependency 這個抽象介面的時候,使用的是 AddScoped 方法來進行註冊,則對於 defaultDependency1 與 defaultDependency2 這兩個參數,將會指向同一個執行個體,而當使用 AddTransient 方法來進行註冊,則對於 defaultDependency1 與 defaultDependency2 這兩個參數,將會指兩個完全不同的執行個體。
這個 ValuesController 類別的建構式內,將會進行注入物件的一些初始化設定,這些設定程式碼將是用來標示這個變數物件名稱是甚麼以及是透過甚麼方式來注入到這個類別內的。
C Sharp / C#
public ValuesController(IMyDependency myDependency1, IMyDependency myDependency2,
    IServiceProvider serviceProvider, IUnityContainer container,
    IDefaultDependency defaultDependency1, IDefaultDependency defaultDependency2)
{
    this.myDependency1 = myDependency1;
    this.myDependency1.InstanceFrom = "控制器的建構式注入";
    this.myDependency1.InstanceName = "myDependency1";
    ...
}
不過,建構函式內的參數僅能夠在建構函式內使用,當結束建構函式執行,呼叫到別的類別方法,就無法再度使用這些建構函式內傳入進來的參數,因此,需要在這個控制器類別內宣告相對應的欄位,用來儲存這些 IoC 容器透過建構函式注入的參數所參考到的執行個體。底下的程式碼將是用來儲存建構函式注入的參數執行個體。
C Sharp / C#
private readonly IMyDependency myDependency1;
private readonly IMyDependency myDependency2;
private readonly IServiceProvider serviceProvider;
private readonly IUnityContainer container;
private readonly IDefaultDependency defaultDependency1;
private readonly IDefaultDependency defaultDependency2;

使用服務定位器 Service Locator 來手動注入實作物件

在某些時候想要自己來透過 IoC 容器取得特定介面的具體實作物件,這個時候就可以使用 服務定位器 service locator pattern 設計模式來做到;在 ASP.NET Core 開發框架中,提供一個 IServiceProvider 介面,只要取得這個介面的具體實作物件,就可以提供一個介面給 IoC 容器,透過 IoC 容器來提供一個具體實作的執行個體。
想要取得 IServiceProvider 介面的具體實作物件可以透過剛剛提到的建構函式注入方法,在該建構函式內提供 IServiceProvider serviceProvider 這個參數宣告,就可以取得 IServiceProvider 介面的具體實作物件。
另外,在 ASP.NET Core 控制器中,可以透過 IServiceProvider contextServiceProvider = HttpContext.RequestServices; 陳述式來取得 IServiceProvider 介面的具體實作物件,另外,也可以透過 IApplicationBuilder 介面中的 ApplicationServices 屬性,一樣是可以取得 IServiceProvider 介面的具體實作物件。
一旦取得了 IServiceProvider 介面的具體實作物件,便可以使用 GetService 或者 GetRequiredService 方法來取得具體實作類別的執行個體,兩者的差異在於當使用前者方法來取得 IoC 容器內的抽象介面之實作物件的時候,若無法找到該介面對應到類別的對應,將會回傳 空值 null;而對於使用後者方法發生同樣的問題,則會拋出例外異常錯誤。
還有在使用 GetService 或者 GetRequiredService 方法的時候,將會發現到使能夠使用非強行別的方法,也就是呼叫這兩個方法的時候,將會回傳一個 object 型別的物件,我們需要自己做型別轉換的工作,因此,建議在這個專案最前面加入這個 using Microsoft.Extensions.DependencyInjection; 命名空間參考,如此當使用這兩個方法的時候,將會看到有泛型多載的強行別方法可以使用,這樣在進行程式設計的時候,許多的一些問題就可以在建置時期發現到設計錯誤的問題,而不用等到執行時期在會看到這些例外異常問題產生,可以大幅增加設計出來程式的可靠度。
然而,許多人對於在開發 ASP.NET Core 專案的時候,並不建議在專案內使用 服務定位器 service locator 功能,因為通稱這樣的設計模式為反模式

使用 Unity 的 IoC 容器用法

對於 ASP.NET Core 開發框架所提供的 IoC 容器,只有提供建構函式方式來注入實作物件,並且對於更多關於相依性注入的應用,並沒有提供;例如:透過屬性方式來注入物件、具名註冊與注入、指定要呼叫的注入建構函式等等。不過,ASP.NET Core 開發框架提供一個擴充機制,讓開發者使用其他好用的 DI 容器程式庫整合到 ASP.NET Core 專案內,使得許多相依性注入的應用與技巧可以在開發專案的時候還使用。

安裝與設定 ASP.NET Core 使用的 Unity IoC 容器

在這裡,將說明如何使用 Unity 這個 DI 容器套件要整合到 ASP.NET Core 專案內的作法,首先,需要安裝 Unity.Microsoft.DependencyInjection 這個 NuGet 套件到專案內,這個範例專案使用現在最新的 2.1.1 版本 。

進行同一個抽象型別對應到多個具體實作型別

接著,需要修正 Startup.cs 檔案內的 Startup 類別;若透過 Visual Studio 2017 建立起一個 ASP.NET Core Web API 類型的專案,在 Startup 類別內將會有個 ConfigureServices 方法,這個方法沒有回傳任何物件,所以,請要修正這個方法,讓 ASP.NET Core 可以使用 Unity 這個 DI 容器。
C Sharp / C#
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
將 ConfigureServices 方法修正如底下程式碼,此時這個 ConfigureServices 方法將會回傳 IServiceProvider 型別的物件。接著 呼叫 Unity.Microsoft.DependencyInjection.ServiceProvider.ConfigureServices(services) 方法取得 Unity 提供的 ServiceProvider 物件,最後,使用呼叫 ConfigureContainer(Container=(UnityContainer)serviceProvider); 方法,使用 IUnityContainer 實作物件來進行抽象介面與類別的註冊工作。最重要的是,要把剛剛得到的 Unity 提供的 ServiceProvider 回傳回去,這樣,ASP.NET Core 系統就可以與 Unity 這個第三方 DI 容器進行整合了。
C Sharp / C#
public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddScoped<IDefaultDependency, MyDefaultClass1>();
    services.AddTransient<IMyDependency, MyClass2>();

    Unity.Microsoft.DependencyInjection.ServiceProvider serviceProvider =
        Unity.Microsoft.DependencyInjection.ServiceProvider.ConfigureServices(services)
        as Unity.Microsoft.DependencyInjection.ServiceProvider;
    ConfigureContainer(Container=(UnityContainer)serviceProvider);
    return serviceProvider;
}

使用具名注入來注入具體實作物件

現在來看看 ConfigureContainer 這個方法做了哪些事情,這個方法是需要另外新建立起來的,當建立好 Unity 容器之後,也就是 IUnityContainer 這個抽象介面的物件,需要透過 IUnityContainer 使用 Unity 容器的用法,進行抽象介面與類別的註冊。在這裡,希望能夠在 ASP.NET Core 專案內,針對 IMyDependency 這個介面要去註冊同時可以對應到 MyClass1 與 MyClass2 這兩個類別,不過,希望設定 MyClass2 為預設注入的類別,也就是說,在進行建構式注入的過程,會預設使用 MyClass2 類別來產生出執行個體。
因此,在這裡需要使用 Unity 的具名註冊的功能,也就是說,需要透過 Unity 容器提供的註冊方法,進行 介面 IMyDependency, 類別 MyClass1 的註冊,但是會提供一個字串到 RetisterType 方法內;所以,日後想要注入 IMyDependency 介面的具體實作物件,但是其為 MyClass1 類型的執行個體,需要 IUnityContainer 的實作物件以及使用前面說明過的 服務定位器 Service Locator 功能,將這個具名字串傳送到解析 Resolve 方法內。
C Sharp / C#
 public void ConfigureContainer(IUnityContainer container)
 {
     // Could be used to register more types
     container.RegisterType<IMyDependency, MyClass1>("MyClass1");
 }
但是,在類別中想要使用具名解析功能的時候,要如何取得 IUnityContainer 介面的實作物件呢?這裡可以透過控制器的建構函式,使用建構式注入 IUnityContainer 的方式,這樣就可以在該控制器類別中使用 IUnityContainer 所提供的各項功能;另外,在這個範例中,當在 ConfigureServices 方法內,使用 Unity.Microsoft.DependencyInjection.ServiceProvider.ConfigureServices 函式產生出一個 IUnityContainer 實作物件之後,之後有使用這樣的 Container=(UnityContainer)serviceProvider 陳述式,將 IUnityContainer 這個具體實作物件,設定到 Startup 類別中的 靜態屬性 Container 上,這樣,就可以在任何類別中,直接透過 Startup.Container 這個靜態屬性來進行相依性注入的解析動作了。
在 ValuesController 類別內的 Get() 方法,可以使用 Startup.Container.Resolve<IMyDependency>("MyClass1"); 表示式,透過 Unity 容器來進行具名註冊,這樣的表示式呼叫完成之後,將會得到一個 MyClass1 類別的執行個體,反過來說,若呼叫 Resolve 方法且沒有傳遞任何字串引數,則將會得到 MyClass2 類別的執行個體。



沒有留言:

張貼留言