相依性插入指導方針

本文提供在 .NET 應用程式中實作相依性插入的一般指導方針和最佳做法。

針對相依性插入設計服務

設計服務以進行相依性插入時:

  • 避免具狀態、靜態類別和成員。 避免設計應用程式改用 singleton 服務來建立全域狀態。
  • 避免直接在服務內具現化相依類別。 直接具現化會將程式碼耦合到特定實作。
  • 讓服務維持在小型、情況良好且可輕鬆測試的狀態。

如果類別有許多插入的相依性,則可能表示類別有太多責任,並且違反單一責任原則 (SRP)。 將類別負責的某些部分移到新的類別,以嘗試重構類別。

處置服務

容器負責清除其所建立的類型,以及在 IDisposable 執行個體上呼叫 Dispose。 開發人員永遠不會處置從容器所解析的服務。 如果類型或 Factory 已註冊為 singleton,則容器會自動處置該 singleton。

在下列範例中,服務是由服務容器所建立,並自動進行處置:

namespace ConsoleDisposable.Example;

public sealed class TransientDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(TransientDisposable)}.Dispose()");
}

上述可處置項目的用途是具有暫時性存留期。

namespace ConsoleDisposable.Example;

public sealed class ScopedDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(ScopedDisposable)}.Dispose()");
}

上述可處置項目的用途是具有限定範圍存留期。

namespace ConsoleDisposable.Example;

public sealed class SingletonDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(SingletonDisposable)}.Dispose()");
}

上述可處置項目的用途是具有 singleton 存留期。

using ConsoleDisposable.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped<ScopedDisposable>();
builder.Services.AddSingleton<SingletonDisposable>();

using IHost host = builder.Build();

ExemplifyDisposableScoping(host.Services, "Scope 1");
Console.WriteLine();

ExemplifyDisposableScoping(host.Services, "Scope 2");
Console.WriteLine();

await host.RunAsync();

static void ExemplifyDisposableScoping(IServiceProvider services, string scope)
{
    Console.WriteLine($"{scope}...");

    using IServiceScope serviceScope = services.CreateScope();
    IServiceProvider provider = serviceScope.ServiceProvider;

    _ = provider.GetRequiredService<TransientDisposable>();
    _ = provider.GetRequiredService<ScopedDisposable>();
    _ = provider.GetRequiredService<SingletonDisposable>();
}

偵錯主控台會在執行下列項目之後顯示下列範例輸出:

Scope 1...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

Scope 2...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

info: Microsoft.Hosting.Lifetime[0]
      Application started.Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
     Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
     Content root path: .\configuration\console-di-disposable\bin\Debug\net5.0
info: Microsoft.Hosting.Lifetime[0]
     Application is shutting down...
SingletonDisposable.Dispose()

服務容器未建立的服務

請考慮下列程式碼:

// Register example service in IServiceCollection
builder.Services.AddSingleton(new ExampleService());

在上述程式碼中:

  • 服務容器「未」建立 ExampleService 執行個體。
  • 此架構「不」會自動處置服務。
  • 開發人員負責處置服務。

暫時性和共用執行個體的 IDisposable 指導

暫時性、有限的存留期

案例

在下列任一情節,應用程式都需要具有暫時性存留期的 IDisposable 執行個體:

  • 此執行個體會在根範圍 (根容器) 中進行解析。
  • 此執行個體應該在範圍結束之前予以處置。

方案

使用 Factory 模式,以在父範圍外部建立執行個體。 在此情況下,應用程式通常會有直接呼叫最終類型建構函式的 Create 方法。 如果最終類型具有其他相依性,則 Factory 可以:

共用執行個體、有限的存留期

案例

應用程式需要跨多個服務的共用 IDisposable 執行個體,但 IDisposable 執行個體應該具有有限的存留期。

方案

使用限定範圍的存留期來註冊執行個體。 使用 IServiceScopeFactory.CreateScope 以建立新的 IServiceScope。 使用範圍的 IServiceProvider 來取得必要的服務。 不再需要範圍時,請處置範圍。

一般 IDisposable 指導方針

  • 請不要註冊具有暫時性存留期的 IDisposable 執行個體。 請改用 Factory 模式。
  • 請不要解析根範圍內具有暫時性或限定範圍存留期的 IDisposable 執行個體。 唯一的例外是,如果應用程式建立/重新建立並處置 IServiceProvider,但這不是理想的模式。
  • 透過 DI 接收 IDisposable 相依性並不需要接收者實作 IDisposable 本身。 IDisposable 相依性接收者不應該針對該相依性呼叫 Dispose
  • 使用範圍來控制服務的存留期。 範圍不是階層式,而且範圍之間沒有特殊連線。

如需資源清除的詳細資訊,請參閱實作 Dispose 方法實作 DisposeAsync 方法。 此外,請考慮容器所擷取的可處置暫時性服務情節,因為這與資源清除有關。

預設服務容器取代

內建服務容器的設計是要服務架構和大部分取用者應用程式的需求。 除非您需要內建容器不支援的特定功能,否則建議使用內建容器,例如:

  • 屬性插入
  • 根據名稱插入 (僅限 .NET 7 和更早版本)。如需詳細資訊,請參閱具有索引鍵的服務
  • 子容器
  • 自訂生命週期管理
  • Func<T> 支援延遲初始設定
  • 慣例型註冊

下列協力廠商容器可以與 ASP.NET Core 應用程式搭配使用:

執行緒安全

建立具備執行緒安全性的 singleton 服務。 如果 singleton 服務相依於暫時性服務,則暫時性服務可能也需要具備執行緒安全性,取決於 singleton 如何使用它。

singleton 服務的 Factory 方法 (例如 AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>) 的第二個引數) 不需要是安全執行緒。 就像類型 (static) 建構函式一樣,單一執行緒只會呼叫它一次。

建議

  • 不支援 async/awaitTask 型服務解析。 因為 C# 不支援非同步建構函式,所以請在同步解析服務之後使用非同步方法。
  • 避免直接在服務容器中儲存資料與設定。 例如,使用者的購物車通常不應該新增至服務容器。 組態應該使用選項模式。 同樣地,請避免只存在以允許存取另一個物件的「資料持有者」物件。 最好是透過 DI 要求實際項目。
  • 避免靜態存取服務。 例如,請避免將 IApplicationBuilder.ApplicationServices 擷取為靜態欄位或屬性,以在其他位置使用。
  • DI Factory 保持快速且同步。
  • 避免使用「服務定位器模式」。 例如,當您可以改用 DI 時,請勿叫用 GetService 來取得服務執行個體。
  • 另一個要避免的服務定位器變化是插入在執行階段解析相依性的 Factory。 這兩種做法都會混用控制反轉策略。
  • 避免在設定服務時呼叫 BuildServiceProvider。 如果開發人員想要在註冊另一個服務時解析某個服務,通常會呼叫 BuildServiceProvider。 請改用多載,其中包含因應此原因的 IServiceProvider
  • 容器會擷取可處置的暫時性服務,以進行處置。 如果從最上層容器解析,則這可能會變成記憶體流失。
  • 啟用範圍驗證,以確定應用程式沒有可擷取限定範圍服務的 singleton。 如需詳細資訊,請參閱範圍驗證

就像所有的建議集,您可能會遇到需要忽略建議的情況。 例外狀況很少見,大部分是架構本身內的特殊案例。

DI 是靜態/全域物件存取模式的「替代」選項。 如果您將 DI 與靜態物件存取混合,則可能無法實現 DI 的優點。

反模式範例

除了本文中的指導方針之外,「您應該避免」數個反模式。 其中有些反模式是學習自開發執行階段本身。

警告

這些是反模式範例、「不」要複製程式碼、「不」要使用這些模式,並避免以所有成本使用這些模式。

容器所擷取的可處置暫時性服務

當您註冊可實作 IDisposable 的「暫時性」服務時,除非在應用程式停止時處置容器 (如果從容器進行解析),或除非處置範圍 (如果從範圍進行解析),否則 DI 容器預設會保留這些參考,而且不對其進行 Dispose()。 如果從容器層級進行解析,則這可能會變成記憶體流失。

Anti-pattern: Transient disposables without dispose. Do not copy!

在上述反模式中,會具現化和根處理 1,000 個 ExampleDisposable 物件。 除非處置 serviceProvider 執行個體,否則不會對其進行處置。

如需針對記憶體流失進行偵錯的詳細資訊,請參閱針對 .NET 中的記憶體流失進行偵錯

非同步 DI Factory 可能會導致死結

"DI Factory" 一詞是指呼叫 Add{LIFETIME} 時存在的多載方法。 有多載可接受 Func<IServiceProvider, T>,其中 T 是正在註冊的服務,而且參數命名為 implementationFactoryimplementationFactory 可以提供作為 Lambda 運算式、本機函數或方法。 如果 Factory 非同步,而且您使用 Task<TResult>.Result,則這會導致死結。

Anti-pattern: Deadlock with async factory. Do not copy!

在上述程式碼中,針對 implementationFactory 提供 Lambda 運算式,其中主體針對 Task<Bar> 傳回方法呼叫 Task<TResult>.Result。 這會「導致死結」GetBarAsync 方法只會使用 Task.Delay 來模擬非同步工作作業,然後呼叫 GetRequiredService<T>(IServiceProvider)

Anti-pattern: Deadlock with async factory inner issue. Do not copy!

如需非同步指導的詳細資訊,請參閱非同步程式設計:重要資訊和建議。 如需針對死結進行偵錯的詳細資訊,請參閱針對 .NET 中的死結進行偵錯

當您執行這個反模式並發生死結時,可以檢視從 Visual Studio 的 [平行堆疊] 視窗等候的兩個執行緒。 如需詳細資訊,請參閱在平行堆疊視窗中檢視執行緒和工作

Captive 相依性

Captive 相依性一詞是由 Mark Seemann 所建立,而且指的是服務存留期的設定錯誤,其中較長的服務會保留較短的服務上限。

Anti-pattern: Captive dependency. Do not copy!

在上述程式碼中,Foo 會註冊為 singleton,並且限定 Bar 的範圍 - 其表面看起來有效。 不過,請考慮 Foo 的實作。

namespace DependencyInjection.AntiPatterns;

public class Foo(Bar bar)
{
}

Foo 物件需要 Bar 物件,以及因為 Foo 是 singleton,並且限定 Bar 範圍 - 這是設定錯誤。 依現狀,只會將 Foo 具現化一次,而且會在其存留期保留 Bar,這比預定限定範圍存留期 Bar 還要長。 您應該將 validateScopes: true 傳遞至 BuildServiceProvider(IServiceCollection, Boolean) 來考慮驗證範圍。 當您驗證範圍時,會收到 InvalidOperationException,以及與「無法從 singleton 'Foo' 取用限定範圍服務 'Bar'」類似的訊息。

如需詳細資訊,請參閱範圍驗證

作為 singleton 的限定範圍服務

使用限定範圍服務時,如果您未建立範圍或不在現有範圍內,則服務會變成 singleton。

Anti-pattern: Scoped service becomes singleton. Do not copy!

在上述程式碼中,會在 IServiceScope 內擷取 Bar,而這是正確做法。 反模式是在範圍外部擷取 Bar,而且變數會命名為 avoid 以顯示哪個範例擷取不正確。

另請參閱