2016 年六月

第 31 卷,第 6 期

本文章是由機器翻譯。

Essential .NET - 使用 .NET Core 進行相依性插入

Mark Michaelis

Mark Michaelis在我的最後兩個發行項,「 記錄與.NET 核心 」 (msdn.com/magazine/mt694089),以及 「 組態在.NET 的核心 」 (msdn.com/magazine/mt632279),我示範了如何運用.NET 核心功能,從 ASP.NET 核心專案 (project.json) 和較常見的.NET 4.6 C# 專案 (*.csproj)。換句話說,充分利用新的架構不限於撰寫 ASP.NET 核心專案的人。在本專欄中我繼續深入.NET 核心,重點在於.NET 核心相依性插入 (DI) 功能,以及它們如何啟用逆轉控制 (IoC) 模式。為之前,利用.NET 核心功能可從 「 傳統 」 CSPROJ 檔案和新興的 project.json 類型專案。範例程式碼中,這次我將使用 XUnit 從 project.json 專案。

為什麼相依性插入嗎?

使用.NET,具現化物件只是一般透過新的運算子 (也就是新 MyService 或任何物件型別是您想要具現化) 的建構函式呼叫。不幸的是,像這樣的引動過程會緊密結合的連接 (硬式編碼參考) 的用戶端 (或應用程式) 的程式碼強制具現化,以及其組件/NuGet 封裝參考的物件。一般的.NET 類型,這不是問題。不過,「 服務 」 記錄、 組態、 付款、 通知或甚至 DI,例如供應項目類型的相依性可能不想要如果您想要切換使用的服務實作。比方說,在某些案例中的用戶端可能使用 NLog 的記錄,雖然它們可能在另一個選擇 Log4Net 或 Serilog。並使用 NLog 的用戶端將不想中途其專案與 Serilog,讓這兩個記錄服務的參考就會造成反效果。

若要解決此問題的硬式編碼至服務實作的參考,DI 提供層級的這類的間接取值,而不是直接與新的運算子,用戶端 (或應用程式) 服務的執行個體化會改為要求的服務集合或 「 原廠 」 執行個體。此外,而不會要求特定的型別 (因此建立緊密結合的參考) 的服務集合,您要求的服務提供者 (在此情況下,在 NLog、 Log4Net 或 Serilog) 會實作介面預期的情況下 (例如 ILoggerFactory) 介面。

結果是當用戶端會直接參考抽象的組件 (Logging.Abstractions),定義服務介面,將會需要直接實作的任何參考。

我們稱解除結合實際的執行個體傳回至用戶端控制項反轉的模式。這是因為,而不會傳回用戶端,決定執行個體化,明確地叫用建構函式,以新的運算子時,DI 決定。DI 註冊用戶端 (通常是介面) 所要求的型別將傳回的型別之間的關聯。此外,DI 通常決定型別的存留期傳回,特別是,是否會有所有要求的型別,而每個要求,或項目之間的新執行個體之間共用的單一執行個體。

DI 一個特別常見需求是在單元測試中。請考慮購物車依賴的服務,反而付費服務。想像一下撰寫運用付款服務購物車 」 服務,並嘗試單元測試的購物車 」 服務,而不實際叫用實際的付費服務。您想要叫用是模擬 (mock) 的付費服務。為了與 DI 達到此目的,您的程式碼會從 DI 架構,而不是呼叫,比方說,新 PaymentService 要求付款服務介面的執行個體。接著,只需要是 「 設定 」 DI 架構,以傳回付款的模擬 (mock) 服務的單元測試。

相反地,生產主機也可以設定購物車,若要使用其中一個 (可能有多個) 的付款服務選項。也可能是最重要的是,參考會只付款抽象概念,而不是每個特定的實作。

提供 「 服務 」 的執行個體,而不需要直接具現化用戶端是 DI 基本原則。而且,事實上,有些 DI 架構允許主應用程式參考實作所支援的組態和反映,而不是編譯時期繫結為基礎的繫結機制分開處理。此種減少方式就是所謂的服務定位器模式。

.NET core Microsoft.Extensions.DependencyInjection

若要利用.NET 核心 DI 架構,您只需要為 Microsoft.Extnesions.DependencyInjection.Abstractions NuGet 封裝參考。這會提供公開 (expose) 您可以從中呼叫 GetService < TService > System.IServiceProvider IServiceCollection 介面的存取。型別參數,TService,識別要擷取的服務 (通常是介面) 的型別,因此應用程式程式碼會取得執行個體 ︰

ILoggingFactory loggingFactor = serviceProvider.GetService<ILoggingFactory>();

沒有對等的非泛型 GetService 方法,做為參數 (而非泛型參數) 型別。泛型方法,讓指派給變數直接特定的型別,而非泛型版本需要明確轉型,因為傳回型別是物件。此外,還有些泛型條件約束加入的服務型別完全時使用的型別參數,就可以避免轉型時。

如果呼叫 GetService 時收集服務中不註冊任何型別,它會傳回 null。這是搭配 null 傳播運算子,將選擇性的行為新增至應用程式時很有用。未登錄的服務型別類似 GetRequiredService 方法會擲回例外狀況。

如您所見,程式碼是很簡單。不過,缺少哪些資訊是如何取得要叫用 GetService 的服務提供者的執行個體。方案是,先具現化 ServiceCollection 的預設建構函式,然後註冊您想要提供的服務類型。範例所示 [圖 1, ,請在您可以假設每個類別 (主機、 應用程式和 PaymentService) 中實作不同的組件。此外,雖然主機組件會知道要使用哪一個記錄器,還有任何參考的應用程式或 PaymentService 記錄器。同樣地,主機組件有任何 PaymentServices 組件的參考。個別 「 抽象 」 組件中也會實作介面。例如,ILogger 介面是 Microsoft.Extensions.Logging.Abstractions 組件中定義。

[圖 1 註冊和相依性插入從要求的物件

public class Host
{
  public static void Main()
  {
    IServiceCollection serviceCollection = new ServiceCollection();
    ConfigureServices(serviceCollection);
    Application application = new Application(serviceCollection);
    // Run
    // ...
  }
  static private void ConfigureServices(IServiceCollection serviceCollection)
  {
    ILoggerFactory loggerFactory = new Logging.LoggerFactory();
    serviceCollection.AddInstance<ILoggerFactory>(loggerFactory);
  }
}
public class Application
{
  public IServiceProvider Services { get; set; }
  public ILogger Logger { get; set; }
    public Application(IServiceCollection serviceCollection)
  {
    ConfigureServices(serviceCollection);
    Services = serviceCollection.BuildServiceProvider();
    Logger = Services.GetRequiredService<ILoggerFactory>()
            .CreateLogger<Application>();
    Logger.LogInformation("Application created successfully.");
  }
  public void MakePayment(PaymentDetails paymentDetails)
  {
    Logger.LogInformation(
      $"Begin making a payment { paymentDetails }");
    IPaymentService paymentService =
      Services.GetRequiredService<IPaymentService>();
    // ...
  }
  private void ConfigureServices(IServiceCollection serviceCollection)
  {
    serviceCollection.AddSingleton<IPaymentService, PaymentService>();
  }
}
public class PaymentService: IPaymentService
{
  public ILogger Logger { get; }
  public PaymentService(ILoggerFactory loggerFactory)
  {
    Logger = loggerFactory?.CreateLogger<PaymentService>();
    if(Logger == null)
    {
      throw new ArgumentNullException(nameof(loggerFactory));
    }
    Logger.LogInformation("PaymentService created");
  }
}

您可以在概念上,做為名稱 / 值組,其中名稱是您將會稍後想要擷取的物件 (通常是介面) 的類型,而值為實作介面的型別或擷取該類型的演算法 (委派) 將 ServiceCollection 型別。Host.ConfigureServices 方法中呼叫 AddInstance, [圖 1, ,因此,註冊的任何要求 ILoggerFactory 類型傳回的 ConfigureServices 方法中建立的相同 LoggerFactory 執行個體。如此一來,應用程式和 PaymentService 就能夠擷取而不需要知道 (或甚至 / NuGet 組件的參考) ILoggerFactory 哪些記錄器是實作和設定。同樣地,應用程式提供不需要知道任何付款服務正在使用哪些 MakePayment 方法。

請注意 ServiceCollection 不直接提供 GetService 或 GetRequiredService 方法。相反地,這些方法可用於從 IServiceProvider ServiceCollection.BuildServiceProvider 方法所傳回的。此外,提供者提供唯一的服務是加入 BuildServiceProvider 的呼叫之前。

在您要建立的情況下的預設執行個體,呼叫 GetService 會傳回 null 或 Microsoft.Framework.DependencyInjection.Abstractions 也包含名為提供一些有用的方法來處理不是向 IServiceProvider,自訂 ObjectFactory 委派建構函式參數的 ActivatorUtilities 靜態 helper 類別 (請參閱 bit.ly/1WIt4Ka#ActivatorUtilities)。

服務的存留期

[圖 1 我叫用 IServiceCollection AddInstance < TService >(TService implementationInstance) 擴充方法。執行個體是四個不同 TService 存留期的選項適用於.NET 核心 DI。它會建立,不只會呼叫 GetService 傳回型別的物件 TService,同時也向 AddInstance 特定 implementationInstance 是會傳回。換句話說,向 AddInstance implementationInstance 特定執行個體,它會傳回每個呼叫 GetService (或 GetRequiredService) 以儲存 AddInstance 方法 TService 型別參數。

相反地,IServiceCollection AddSingleton < TService > 擴充方法的執行個體沒有參數,並改為依賴 TService 擁有具現化,透過建構函式的表示。預設建構函式的運作方式,Microsoft.Extensions.DependencyInjection 也支援非預設建構函式也會登錄其參數。例如,您可以呼叫 ︰

IPaymentService paymentService = Services.GetRequiredService<IPaymentService>()

和 DI 會負責擷取 ILoggingFactory 具象執行個體,並利用它需要在其建構函式 ILoggingFactory PaymentService 類別具現化時。

如果沒有這類方法 TService 類型中,您可以改為運用的多載會接受委派的型別 Func < IServiceProvider,TService > implementationFactory AddSingleton 擴充方法,以具現化 TService factory 方法。是否與否,您可以提供 factory 方法,可確保服務集合實作它永遠只會建立 TService 類型,以確保沒有單一執行個體的執行個體。GetService 第一次呼叫之後,就會觸發 TService 具現化相同的執行個體一律會傳回服務集合的存留期。

IServiceCollection 也包括 AddTransient (serviceType 類型、 類型 implementationType) 和 AddTransient (serviceType 類型、 Func < IServiceProvider,TService > implementationFactory) 擴充方法。這些是 AddSingleton 類似,只是它們傳回的新執行個體,每次他們要叫用,確保您一定 TService 類型的新執行個體。

最後,有數個 AddScoped 類型的擴充方法。這些方法的設計,以傳回相同的執行個體中指定的內容,並建立新執行個體內容 — 稱為範圍 — 變更。ASP.NET 核心的行為在概念上會對應到已設定領域的存留期。基本上,每個 HttpContext 執行個體,建立新的執行個體與相同的 HttpContext 內呼叫 GetService 時,會傳回相同的 TService 執行個體。

在 [摘要] 共有四個服務集合實作所傳回的物件的存留期選項 ︰ 執行個體、 單一值,暫時性的並且已設定領域。ServiceLifetime 列舉中所定義的最後三個 (bit.ly/1SFtcaG)。執行個體,不過,缺少,因為它是特殊形式的 Scoped 內容不會改變。

先前我參考,在概念上類似的名稱 / 值組 ServiceCollection TService 型別做為查閱。ServiceDescription 類別中完成 ServiceCollection 型別的實際實作 (請參閱 bit.ly/1SFoDgu)。這個類別會提供所需的資訊來具現化 TService 容器,也就是 ServiceType (TService)、 ImplementationType 或 ImplementationFactory 委派以及 ServiceLifetime。除了 ServiceDescriptor 建構函式,還有一堆 ServiceDescriptor 上協助進行具現化 ServiceDescriptor 本身的靜態 factory 方法。

不論您註冊與您 TService 的存留期,TService 本身必須是參考型別,不是值型別。當您使用的型別參數為 TService (而非傳遞做為參數的型別) 編譯器會驗證此泛型類別條件約束。一件事,但是,不會驗證使用 TService 物件類型。您將以確定想要避免這點,以及其他非唯一的介面 (例如 IComparable,或許是)。原因是,如果您註冊的型別物件,不論您指定 GetService 引動過程中哪些 TService 物件註冊為一律會傳回 TService 型別。

DI 實作 (implementation) 的相依性插入

ASP.NET 利用 DI,事實上,您可以 DI DI 架構本身中不斷累積。換句話說,您不一定要使用位於 Microsoft.Extensions.DependencyInjection DI 機制 ServiceCollection 實作。相反地,只要您有的類別會實作 IServiceCollection (定義於 Microsoft.Extensions.DependencyInjection.Abstractions; 請參閱 bit.ly/1SKdm1z) 或 IServiceProvider (系統命名空間內定義的.NET 核心 lib 架構) 可以換成您自己 DI 架構,或利用其中一個其他確立 DI 架構包括 Ninject (ninject.org, ,辦公室出 @IanfDavis 心血維護這麼多年來使用) 和 Autofac (autofac.org)。

ActivatorUtilities 上文字

Microsoft.Framework.DependencyInjection.Abstractions 也包含一個靜態 helper 類別,提供一些有用的方法,在建構函式參數不是向 IServiceProvider、 自訂 ObjectFactory 委派或您要建立的預設執行個體,該呼叫 GetService 會傳回 null 的情況下使用。您可以找到此公用程式類別使用 MVC framework 和 SignalR 的文件庫中的位置。在第一個案例中,使用簽章的 CreateInstance 方法 < T > (IServiceProvider 提供者,params 物件 [] parameters) 存在,可讓您向 DI 架構未登錄的引數的型別傳入建構函式參數。您可能也需要效能是編譯的 lambda 的需求,lambda 函式需要以產生您的型別。在此情況下傳回 ObjectFactory CreateFactory (instanceType 型別、 型別 [] argumentTypes) 方法可以是很有用。第一個引數是在取用者所搜尋到型別,且第二個引數中的所有建構函式類型,順序,符合您想要使用的第一個類型的建構函式。在其 實作, ,這些項目會緊縮至已編譯的 lambda 會極高效能多次呼叫時。最後,GetServiceOrCreateInstance < T >(IServiceProvider provider) 方法提供簡單的方法提供的類型,可能會選擇性地中已註冊的不同位置的預設執行個體。這是在案例中特別有用,您允許 DI 之前引動過程,但如果不會發生的您有後援實作。

總結

與.NET Core 記錄和組態,.NET Core DI 機制提供簡單的實作,它的功能。當您在尋找更多進階的 DI 功能的一些其他架構不太可能時,.NET Core 版本為輕量型開始的好方法。此外 (甚至是同樣的例如記錄和組態),.NET Core 實作來取代更進步的實作。因此,您可以考慮利用的 「 包裝函式 」 讓您可以插入其他 DI 架構在需要時在未來的.NET Core DI 架構。如此一來,您不需要定義您自己 「 自訂 」 DI 的包裝函式,但可以利用.NET Core 為標準的任何用戶端/應用程式可以插入自訂實作。

請注意有關 ASP.NET Core 是它會利用整個 DI。如果您需要它,而且當嘗試換成您的單元測試中的程式庫的模擬 (mock) 實作,它會特別重要,這是一定很好的做法。缺點是,而不是新的運算子的建構函式的簡單呼叫,需要 DI 註冊和 GetService 呼叫的複雜性。我忍不住想知道是否或許是 C# 語言可以簡化此但,根據目前的 C# 7.0 設計,未發生任何休閒時間。


Mark Michaelis是的 IntelliTect,他擔任其技術架構設計人員和培訓講師的創辦人。近二十他 Microsoft MVP 和 Microsoft 區域經理自已 2007年。Michaelis 服務於多個 Microsoft 軟體設計檢閱小組,包括 C#、 Microsoft Azure、 SharePoint 和 Visual Studio ALM。他開發人員會議上發表演說,並且已撰寫許多本書,包括他最新,「 基本 C# 6.0 (第 5 版) 」 (itl.tc/EssentialCSharp)。在 Facebook 上連絡他 facebook.com/Mark.Michaelis, ,他的部落格上 IntelliTect.com/Mark, ,在 Twitter 上: @markmichaelis 或透過電子郵件地 mark@IntelliTect.com

感謝下列 IntelliTect 技術專家來檢閱這份文件 ︰ Kelly Adams、 Kevin Bost、 Ian Davis 和 Phil Spokas