(摘)使用 .NET Core 實現依賴關係註入

来源:https://www.cnblogs.com/mlinber/archive/2018/10/08/9754262.html
-Advertisement-
Play Games

為什麼使用依賴關係註入? 使用 .NET,通過 new 運算符(即,new MyService 或任何想要實例化的對象類型)調用構造函數即可輕鬆實現對象實例化。遺憾的是,此類調用會強制實施客戶端(或應用程式)代碼到已實例化對象的緊密耦合的連接(硬編碼的引用),此外還會引用其程式集/NuGet 包。對 ...


為什麼使用依賴關係註入?

使用 .NET,通過 new 運算符(即,new MyService 或任何想要實例化的對象類型)調用構造函數即可輕鬆實現對象實例化。遺憾的是,此類調用會強制實施客戶端(或應用程式)代碼到已實例化對象的緊密耦合的連接(硬編碼的引用),此外還會引用其程式集/NuGet 包。對於常見的 .NET 類型而言,這不是問題。然而,對於提供“服務”(如日誌記錄、配置、支付、通知或事件 DI)的類型,如果你想切換所用服務的實現,則可能不需要依賴關係。例如,一種方案是,客戶端可能將 NLog 用於日誌記錄,而另一種方案是,客戶端可能選擇 Log4Net 或 Serilog。而且,使用 NLog 的客戶端不喜歡使用 Serilog 打亂其項目,因此,同時引用兩種日誌記錄服務不會令人滿意。

為瞭解決對服務實現的引用進行硬編碼的問題,DI 提供了一個間接層,這樣與其直接使用 new 運算符實例化服務,倒不如客戶端(或應用程式)請求實例的服務集或“工廠”。此外,與其請求特定類型的服務集(例如創建一個緊密耦合的引用),倒不如請求一個介面(如 ILoggerFactory),並期待服務提供程式(本例中為 NLog、Log4Net 或 Serilog)實現該介面。

結果是,當客戶端直接引用抽象程式集 (Logging.Abstractions) 時,會同時定義服務介面­,將不需要引用直接實現。

我們將解耦返回到客戶端的實際實例的模式稱為控制反轉。這是因為,與其客戶端確定要實例化的對象,就像使用 new 運算符顯式調用構造函數時一樣,倒不如 DI 確定將返回的內容。DI 註冊了由客戶端請求的類型(一般為介面)和將返回的類型之間的關聯。此外,DI 通常會確定已返回類型的生存期,具體取決於該類型的所有請求之間將有單個共用的實例、每個請求將各有一個新實例,還是介於兩者之間。

對 DI 的一個尤為常見的需求體現在單元測試中。考慮相應地取決於付款服務的購物車服務。假設編寫利用付款服務的購物車服務,並嘗試對購物車服務進行單元測試,而不實際調用真實的付款服務。相反,你想調用的是模擬付款服務。為了使用 DI 實現此目的,你的代碼會從 DI 框架請求付款服務介面的實例而不是調用,例如,new PaymentService。然後,只需為單元測試“配置”DI 框架,以返回一個模擬付款服務。

相比之下,生產主機可以配置購物車,以使用(可能很多)付款服務選項之一。也許最重要的是,引用將僅針對付款抽象,而不是針對每個具體的實現。

提供“服務”的實例而不是使客戶端直接將其實例化是 DI 的基本原則。事實上,一些 DI 框架允許通過支持基於配置和反射的綁定機制(而不是編譯時綁定)從引用實現中對主機進行解耦。這種解耦稱為服務定位器模式。

.NET Core Microsoft.Extensions.DependencyInjection

若要利用 .NET Core DI 框架,你只需引用 Microsoft.Extnesions.DependencyInjection.Abstractions NuGet 包。此包提供了 IServiceCollection 介面的入口,從而公開你可以從中調用 GetService<TService> 的 System.IService­Provider。類型參數 TService 標識要檢索的服務的類型(一般為介面),如下應用程式代碼獲得了一個實例:

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

有一些相應的非泛型 GetService 方法將 Type 作為參數(而不是泛型參數)。泛型方法允許直接分配給特定類型的變數,而非泛型版本需要一個顯式轉換,因為返回類型為 Object。此外,當添加該服務類型時,會有泛型約束,因此使用該類型參數時可以完全避免轉換。

如果在調用 GetService 時沒有使用收集服務註冊任何類型,它將返回 null。這在與 null 傳播運算符結合以將可選行為添加到應用時非常有用。類似的 GetRequiredService 方法在沒有註冊服務類型時會拋出異常。

如你所見,代碼非常簡單。然而,現在缺少的是如何獲得在其上調用 GetService 的服務提供程式的實例。解決方案是首先實例化 ServiceCollection 的預設構造函數,然後再註冊你想要服務提供的類型。圖 1 中顯示了一個示例,你可以假設其中的每個類(Host、Application 和 PaymentService)已在單獨的程式集中實現。此外,儘管 Host 程式集知道要使用哪個記錄器,但是沒有在 Application 或 PaymentService 中引用記錄器。同樣,Host 程式集沒有引用 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 類型認為是名稱/值對,其中名稱是稍後將要檢索的對象的類型(一般為介面),而值是實現介面的類型或用於檢索該類型的演算法(委托)。因此,在圖 1 的 Host.Configure­Services 方法中調用 AddInstance 可註冊 ILoggerFactory 類型的任何請求,該類型返回在 ConfigureServices 方法中創建的相同 LoggerFactory 實例。因此,Application 和 PaymentService 均可以檢索 ILoggerFactory,而無需瞭解實現和配置記錄器的知識(或程式集/NuGet 引用)。同樣,Application 提供 MakePayment 方法,無需瞭解關於要使用的付款服務的知識。

請註意,ServiceCollection 不直接提供 GetService 或 GetRequiredService 方法。而是由 ServiceCollection.BuildServiceProvider 方法返回的 IServiceProvider 提供這些方法。此外,僅由提供程式提供的服務是調用 BuildServiceProvider 之前添加的服務。

Microsoft.Framework.DependencyInjection.Abstractions 還包括稱為 ActivatorUtilities 的靜態幫助程式類,該類提供了一些有用的方法,用於處理未使用 IServiceProvider(自定義的 ObjectFactory 委托)註冊的構造函數參數,或者在想要創建預設實例的情況下,調用 GetService 時返回 null(請參閱 bit.ly/1WIt4Ka#ActivatorUtilities)。

服務生存期

圖 1 中,我調用了 IServiceCollection AddInstance<TService>(TService implementationInstance) 擴展方法。Instance 是 .NET Core DI 附帶的四個不同的 TService 生存期選項之一。它規定不僅 GetService 的調用將返回 TService 類型的對象,而且將返回使用 AddInstance 註冊的特定 implementationInstance 實例。換句話說,使用 AddInstance 進行註冊可以保存特定的 implementationInstance 實例,因此每次使用 AddInstance 方法的 TService 類型參數調用 GetService(或 GetRequiredService)時均可以返回該實例。

相反,IServiceCollection AddSingleton<TService> 擴展方法沒有實例參數,而是依賴於通過構造函數進行實例化的 TService。預設的構造函數有效,Microsoft.Extensions.Dependency­Injection 也支持註冊了參數的非預設構造函數。例如,你可以調用:

 
IPaymentService paymentService = Services.GetRequiredService<IPaymentService>()

而且,在實例化需要其構造函數中的 ILoggingFactory 的 PaymentService 類時,DI 將負責檢索具體的 ILoggingFactory 實例並利用該實例。

如果 TService 類型中沒有此類方法可用,則可以重載 AddSingleton 擴展方法,該方法採用了 Func<IServiceProvider, TService> implementationFactory(用於實例化 TService 的工廠方法)類型的委托。無論你是否提供工廠方法,服務收集實現都會確保將僅創建一個 TService 類型的實例,從而確保存在單一實例。在第一次調用觸發 TService 實例的 GetService 後,在服務收集的生存期內將始終返回同一實例。

IServiceCollection 還包括 AddTransient(Type serviceType, Type implementationType) 和 AddTransient(Type serviceType, Func<IServiceProvider, TService> implementationFactory) 擴展方法。這些方法類似於 AddSingleton,不同的是每次調用這些方法時都會返回一個新實例,從而確保你始終擁有 TService 類型的新實例。

最後,有幾個 AddScoped 類型的擴展方法。這些方法設計為在給定的上下文中返回同一實例,並且每當上下文(也稱為作用域)更改時都會創建新實例。從概念上講,ASP.NET Core 的行為映射到作用域生存期。從本質上講,新實例是針對每個 HttpContext 實例創建的,而且每當在相同的 HttpContext 內調用 GetService 時,都會返回完全相同的 TService 實例。

總之,有四個生存期選項,用於從服務收集實現返回的對象: Instance、Singleton、Transient 和 Scoped。最後三個是在 ServiceLifetime 枚舉中定義的 (bit.ly/1SFtcaG)。但是,缺少 Instance,因為它是 Scoped(在其中無法更改上下文)的特殊用例。

之前我提到過 ServiceCollection 在概念上就像一個名稱/值對,它將 TService 類型用於查找。ServiceCollection 類型的實際實現在 ServiceDescription 類中完成(請參閱 bit.ly/1SFoDgu)。該類為實例化 TService(即,ServiceType (TService))、Implementation­Type 或 ImplementationFactory 委托以及 ServiceLifetime 所需的信息提供了一個容器。除了 ServiceDescriptor 構造函數,ServiceDescriptor 上還有許多靜態工廠方法,可幫助實例化 ServiceDescriptor 本身。

無論使用哪種生存期註冊 TService,TService 本身必須是一個引用類型,而不是值類型。每當你將類型參數用於 TService(而不是作為參數傳遞 Type)時,編譯器都會使用泛型類約束進行驗證。然而,編譯器不會驗證是否使用的是對象類型 TService。你一定要避免這種情況,以及任何其他非獨特的介面(或許如 IComparable)。原因是,如果你註冊了對象類型的內容,無論你在 GetService 調用中指定哪種類型的 TService,將始終返回註冊為 TService 類型的對象。

DI 實現的依賴關係註入

ASP.NET 利用 DI 的程度之深,事實上,你可以在 DI 框架本身內實現 DI。換句話說,你不限於使用在 Microsoft.Extensions.DependencyInjection 中發現的 DI 機制的 ServiceCollection 實現。相反,只要你有實現 IServiceCollection(在 Microsoft.Extensions.DependencyInjection.Abstractions 中定義,請參閱 bit.ly/1SKdm1z)或IServiceProvider(在 .NET Core lib 框架的 System 命名空間內定義)的類,你就可以替代自己的 DI 框架或利用另外一個完善的 DI 框架,其中包括 Ninject(ninject.org,經過數年的努力維護 @IanfDavis 呼之欲出)和 Autofac (autofac.org)。

淺談 ActivatorUtilities

Microsoft.Framework.DependencyInjection.Abstractions 還包括靜態幫助程式類,該類提供了一些有用的方法,用於處理未使用 IServiceProvider(自定義的 ObjectFactory 委托)註冊的構造函數參數,或者在想要創建預設實例的情況下,調用 GetService 時返回 null。你可以找到一些在 MVC 框架和 SignalR 庫中使用此實用工具類的示例。在第一種情況下,存在一個帶有 CreateInstance<T>(IServiceProvider provider, params object[] parameters) 簽名的方法,允許你針對未註冊的參數使用 DI 框架將構造函數參數傳入到註冊的類型中。你可能還會有性能需求,lambda 函數需要生成已編譯的 lambda 類型。返回 ObjectFactory 的 CreateFactory(Type instanceType, Type[] argumentTypes) 方法在這種情況下可能有用。第一個參數是用戶尋求的類型,而第二個參數是所有的構造函數類型,以匹配你希望使用的第一個類型的構造函數。在其實現中,這些片段都精簡到已編譯的 lambda,多次調用後,性能會相當高。最後,GetServiceOrCreateInstance<T>(IServiceProvider provider) 方法提供了一個簡單方式,用於提供可能已選擇在其他地方註冊的類型的預設實例。這在調用之前允許 DI 的情況下尤為有用,但是,如果未發生這種情況,你會獲得一個回退實現。

總結

與 .NET Core 日誌記錄和配置一樣,.NET Core DI 機制提供了一個相對簡單的功能實現。雖然你不可能找到其他一些框架的更高級的 DI 功能,但 .NET Core 版本是輕量級的,並且是一個很好的入門方式。此外(再如日誌記錄和配置),.NET Core 實現可以被一個更成熟的實現替代。因此,你可能會考慮利用 .NET Core DI 框架作為一個“包裝器”,通過它,將來你可以根據需要插入其他 DI 框架。通過這種方式,你不必定義自己的“自定義”DI 包裝器,但可以利用 .NET Core 的包裝器作為標準,任何客戶端/應用程式都可以為標準的包裝器插入自定義的實現。

關於 ASP.NET Core 需要註意的是,它自始至終都在利用 DI。這無疑是一個重大實踐,在單元測試中嘗試替代庫的模擬實現時,如果你需要它,它會尤為重要。缺點是,並非簡單的調用帶有 new 運算符的構造函數,DI 註冊和 GetService 調用的複雜性是必要的。我不禁想知道,C# 語言是否可以簡化這種複雜性,但是,基於目前的 C# 7.0 設計,要實現這一點並不容易。

 

原文:https://msdn.microsoft.com/zh-cn/magazine/mt707534.aspx


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 內容 本文對JDK1.7下使用segmentShift和segmentMask求解ConcurrentHashMap鍵值對在Segment[]中的下標值進行了探究和論證。 適合人群 ​ Java進階 說明 轉載請註明出處,尊重筆者的勞動成果。 推薦閱讀 探究HashMap線性不安全(二)——鏈表成環 ...
  • 題意 "題目鏈接" Sol 只要知道“迴文連續子串”就能做了吧。。 想要滿足這個條件,肯定是不能出現$aa$或$aba$這種情況 如果沒有$S$的限制,答案為$K (K 1) \prod_{i = 3}^n (k 2)$ 如果有$S$的限制就除一個$K$ 然而考場上沒註意到會乘爆long long於 ...
  • 主要內容:1. 模塊的簡單認識2. collections模塊3. time時間模塊4. random模塊5. os模塊6. sys模塊 一. 模塊的簡單認識什麽是模塊. 模塊就是我們把裝有特定功能的代碼進行歸類的結果. 從代碼編寫的單位來看我們的程式, 從小到大的順序: 一條代碼 < 語句句塊 < ...
  • 直接上代碼 HTML頁面代碼: controller.js代碼 webapi代碼: 有疑問歡迎交流。 ...
  • 現在的開發中越來越看重依賴註入的思想,微軟的 Asp.Net Core 框架更是天然集成了依賴註入,那麼在單元測試中如何使用依賴註入呢? 本文主要介紹如何通過 XUnit 來實現依賴註入, XUnit 主要藉助 SharedContext 來共用一部分資源包括這些資源的創建以及釋放。 ...
  • 摘要:上篇寫瞭如何搭建一個簡單項目框架的上部分,講了關於Dal和Bll之間解耦的相關知識,這篇來把後i面的部分說一說。 上篇講到DbSession,現在接著往下講。 首先,還是把一些類似的操作完善一下,與Dal層相同,我們同樣可以把Bll層中某些使用廣泛的類似的操作封裝到基類中,另外,同樣要給Bll ...
  • 一、DotNetty背景介紹 某天發現 dotnet 是個好東西,就找了個項目來練練手。於是有了本文的 Mqtt 客戶端 (github: MqttFx ) DotNetty是微軟的Azure團隊,使用C#實現的Netty的版本發佈。不但使用了C#和.Net平臺的技術特點,並且保留了Netty原來絕 ...
  • OpenID Connect執行終端用戶登錄或確定終端用戶已經登錄的驗證工作。OpenID Connect 使伺服器以一種安全的方式返回驗證結果。所以客戶可以依靠它。出於這個原因,在這種情況下客戶被稱為依賴方(RP)。 驗證結果在返回ID令牌中,ID令牌定義(第二節)。它聲明表達這些信息作為發行人, ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...