相依性插入指導方針
本文提供在 .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 可以:
- 在其建構函式中接收 IServiceProvider。
- 使用 ActivatorUtilities.CreateInstance 以在容器外部將執行個體具現化,同時將容器用於其相依性。
共用執行個體、有限的存留期
案例
應用程式需要跨多個服務的共用 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/await
和Task
型服務解析。 因為 C# 不支援非同步建構函式,所以請在同步解析服務之後使用非同步方法。 - 避免直接在服務容器中儲存資料與設定。 例如,使用者的購物車通常不應該新增至服務容器。 組態應該使用選項模式。 同樣地,請避免只存在以允許存取另一個物件的「資料持有者」物件。 最好是透過 DI 要求實際項目。
- 避免靜態存取服務。 例如,請避免將 IApplicationBuilder.ApplicationServices 擷取為靜態欄位或屬性,以在其他位置使用。
- 讓 DI Factory 保持快速且同步。
- 避免使用「服務定位器模式」。 例如,當您可以改用 DI 時,請勿叫用 GetService 來取得服務執行個體。
- 另一個要避免的服務定位器變化是插入在執行階段解析相依性的 Factory。 這兩種做法都會混用控制反轉策略。
- 避免在設定服務時呼叫 BuildServiceProvider。 如果開發人員想要在註冊另一個服務時解析某個服務,通常會呼叫
BuildServiceProvider
。 請改用多載,其中包含因應此原因的IServiceProvider
。 - 容器會擷取可處置的暫時性服務,以進行處置。 如果從最上層容器解析,則這可能會變成記憶體流失。
- 啟用範圍驗證,以確定應用程式沒有可擷取限定範圍服務的 singleton。 如需詳細資訊,請參閱範圍驗證。
就像所有的建議集,您可能會遇到需要忽略建議的情況。 例外狀況很少見,大部分是架構本身內的特殊案例。
DI 是靜態/全域物件存取模式的「替代」選項。 如果您將 DI 與靜態物件存取混合,則可能無法實現 DI 的優點。
反模式範例
除了本文中的指導方針之外,「您應該避免」數個反模式。 其中有些反模式是學習自開發執行階段本身。
警告
這些是反模式範例、「不」要複製程式碼、「不」要使用這些模式,並避免以所有成本使用這些模式。
容器所擷取的可處置暫時性服務
當您註冊可實作 IDisposable 的「暫時性」服務時,除非在應用程式停止時處置容器 (如果從容器進行解析),或除非處置範圍 (如果從範圍進行解析),否則 DI 容器預設會保留這些參考,而且不對其進行 Dispose()。 如果從容器層級進行解析,則這可能會變成記憶體流失。
在上述反模式中,會具現化和根處理 1,000 個 ExampleDisposable
物件。 除非處置 serviceProvider
執行個體,否則不會對其進行處置。
如需針對記憶體流失進行偵錯的詳細資訊,請參閱針對 .NET 中的記憶體流失進行偵錯。
非同步 DI Factory 可能會導致死結
"DI Factory" 一詞是指呼叫 Add{LIFETIME}
時存在的多載方法。 有多載可接受 Func<IServiceProvider, T>
,其中 T
是正在註冊的服務,而且參數命名為 implementationFactory
。 implementationFactory
可以提供作為 Lambda 運算式、本機函數或方法。 如果 Factory 非同步,而且您使用 Task<TResult>.Result,則這會導致死結。
在上述程式碼中,針對 implementationFactory
提供 Lambda 運算式,其中主體針對 Task<Bar>
傳回方法呼叫 Task<TResult>.Result。 這會「導致死結」。 GetBarAsync
方法只會使用 Task.Delay 來模擬非同步工作作業,然後呼叫 GetRequiredService<T>(IServiceProvider)。
如需非同步指導的詳細資訊,請參閱非同步程式設計:重要資訊和建議。 如需針對死結進行偵錯的詳細資訊,請參閱針對 .NET 中的死結進行偵錯。
當您執行這個反模式並發生死結時,可以檢視從 Visual Studio 的 [平行堆疊] 視窗等候的兩個執行緒。 如需詳細資訊,請參閱在平行堆疊視窗中檢視執行緒和工作。
Captive 相依性
Captive 相依性一詞是由 Mark Seemann 所建立,而且指的是服務存留期的設定錯誤,其中較長的服務會保留較短的服務上限。
在上述程式碼中,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。
在上述程式碼中,會在 IServiceScope 內擷取 Bar
,而這是正確做法。 反模式是在範圍外部擷取 Bar
,而且變數會命名為 avoid
以顯示哪個範例擷取不正確。