相依性插入指導方針

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

針對相依性插入設計服務

針對相依性插入設計服務時:

  • 避免具狀態、靜態類別和成員。 請改為使用單一服務來設計應用程式,以避免建立全域狀態。
  • 避免直接在服務內具現化相依類別。 直接具現化會將程式碼耦合到特定實作。
  • 使服務更小、妥善組成且輕鬆地進行測試。

如果類別有許多插入的相依性,可能是因為類別具有太多責任,而且違反了 單一責任原則 (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()");
}

先前的可處置是為了具有單一存留期。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace ConsoleDisposable.Example;

class Program
{
    static async Task Main(string[] args)
    {
        using IHost host = CreateHostBuilder(args).Build();

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

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

        await host.RunAsync();
    }

    static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureServices((_, services) =>
                services.AddTransient<TransientDisposable>()
                        .AddScoped<ScopedDisposable>()
                        .AddSingleton<SingletonDisposable>());

    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>();
    }
}

在執行之後,debug 主控台會顯示下列範例輸出:

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()

服務容器未建立的服務

請考慮下列程式碼:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton(new ExampleService());

    // ...
}

在上述程式碼中:

  • ExampleService 實例不是由服務容器ExampleService
  • 架構 不會自動處置服務
  • 開發人員負責處置服務。

暫時性和共用實例的 IDisposable 指引

暫時性、有限存留期

案例

應用程式需要 IDisposable 在下列任一案例中具有暫時性存留期的實例:

  • (根容器) 的根範圍中解析實例。
  • 實例應該在範圍結束之前處置。

方案

使用 factory 模式,在父範圍之外建立實例。 在這種情況下,應用程式通常會有 Create 方法可直接呼叫最終型別的函式。 如果最終類型有其他相依性,factory 可以:

共用實例,有限存留期

案例

應用程式需要跨多個服務的共用 IDisposable 實例,但實例的 IDisposable 存留期應受限。

方案

註冊具有範圍存留期的實例。 使用 IServiceScopeFactory.CreateScope 建立新 IServiceScope 的。 使用範圍 IServiceProvider 來取得所需的服務。 不再需要時處置範圍。

一般 IDisposable 指導方針

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

如需資源清除的詳細資訊,請參閱 執行 方法或 執行 方法。 此外,請考慮容器案例所捕捉的可 處置暫時性服務 ,因為它與資源清理相關。

預設服務容器取代

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

  • 屬性插入
  • 根據名稱插入
  • 子容器
  • 自訂生命週期管理
  • Func<T> 支援延遲初始設定
  • 以慣例為基礎的註冊

下列協力廠商容器可搭配 ASP.NET Core apps 使用:

執行緒安全

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

單一服務的 factory 方法(例如 >addsingleton 別 tservice > (IServiceCollection、Func < IServiceProvider、別 tservice >) 的第二個引數)不需要是安全線程。 如同型別 (static) 的函式,它保證只能由單一線程呼叫一次。

建議

  • async/awaitTask 不支援以服務為基礎的服務解析。 因為 c # 不支援非同步函式,所以請在以同步方式解析服務後使用非同步方法。
  • 避免直接在服務容器中儲存資料與設定。 例如,使用者的購物車通常不應該新增至服務容器。 組態應該使用選項模式。 同樣地,請避免只存在於允許存取另一個物件的「資料持有者」物件。 最好是透過 DI 要求實際項目。
  • 避免靜態存取服務。 例如,請避免將 IApplicationBuilder >iapplicationbuilder.applicationservices 為靜態欄位或屬性,以便在其他地方使用。
  • DI factory 保持快速且同步。
  • 請避免使用 服務定位器模式。 例如,當您可以改用 DI 時,請勿叫用 GetService 來取得服務執行個體。
  • 另一個要避免的服務定位器變異,是插入在執行時間解析相依性的 factory。 這兩種做法都會混用控制反轉策略。
  • 避免在中 ConfigureServices 呼叫 BuildServiceProvider 。 呼叫 BuildServiceProvider 通常會在開發人員想要解析中 ConfigureServices 的服務時發生。
  • 容器會捕獲可處置的暫時性服務以供處置。 如果從最上層容器解析,這可能會導致記憶體流失。
  • 啟用範圍驗證,以確定應用程式沒有可捕獲範圍服務的 singleton。 如需詳細資訊,請參閱範圍驗證

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

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

範例反模式

除了本文中的指導方針之外,還有數個應該避免的反模式。 其中有些反模式是從開發執行時間本身學習而來的。

警告

這些是範例的反模式 ,不會複製程式 代碼、 不使用這些 模式,並以所有成本來避免這些模式。

容器所捕獲的可處置暫時性服務

當您註冊執行 暫時性服務時,根據預設,DI 容器會保留這些參考,而不 Dispose() 會保留這些參考,直到當應用程式停止時,如果已從容器中解析容器,或直到範圍已從範圍內處置為止。 如果從容器層級解析,這可能會導致記憶體流失。

static void TransientDisposablesWithoutDispose()
{
    var services = new ServiceCollection();
    services.AddTransient<ExampleDisposable>();
    ServiceProvider serviceProvider = services.BuildServiceProvider();

    for (int i = 0; i < 1000; ++ i)
    {
        _ = serviceProvider.GetRequiredService<ExampleDisposable>();
    }

    // serviceProvider.Dispose();
}

在上述的反模式中,1000 ExampleDisposable 物件具現化和根目錄。 在實例處置之前 serviceProvider ,它們將不會被處置。

如需有關偵錯工具記憶體流失的詳細資訊,請參閱 在 .net 中偵測記憶體流失。

非同步 DI 處理站可能會造成鎖死

「DI factory」一詞是指呼叫 Add{LIFETIME} 時存在的多載方法。 有多載接受 Func<IServiceProvider, T> where T 是註冊的服務,並命名 implementationFactory 參數。 implementationFactory可以做為 lambda 運算式、區域函數或方法來提供。 如果 factory 是非同步,而且您使用 Task<TResult>.Result ,這會造成鎖死。

static void DeadLockWithAsyncFactory()
{
    var services = new ServiceCollection();
    services.AddSingleton<Foo>(implementationFactory: provider =>
    {
        Bar bar = GetBarAsync(provider).Result;
        return new Foo(bar);
    });

    services.AddSingleton<Bar>();

    using ServiceProvider serviceProvider = services.BuildServiceProvider();
    _ = serviceProvider.GetRequiredService<Foo>();
}

在上述程式碼中, implementationFactory 會指定 lambda 運算式,其中主體會在傳回的方法上 Task<Bar> 呼叫 Task<TResult>.Result 。 這 會造成鎖死GetBarAsync方法只會使用 Task.Delay 來模擬非同步作業作業,然後再呼叫 GetRequiredService<T>(IServiceProvider)

static async Task<Bar> GetBarAsync(IServiceProvider serviceProvider)
{
    // Emulate asynchronous work operation
    await Task.Delay(1000);

    return serviceProvider.GetRequiredService<Bar>();
}

如需非同步指引的詳細資訊,請參閱 非同步程式設計:重要資訊和建議。 如需偵錯工具鎖死的詳細資訊,請參閱 在 .net 中偵測鎖死

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

相關性

「驗證相依性」一詞是由Mark Seemann所創造,並指的是服務存留期的設定不正確,其中長期的服務會保留較短的服務。

static void CaptiveDependency()
{
    var services = new ServiceCollection();
    services.AddSingleton<Foo>();
    services.AddScoped<Bar>();

    using ServiceProvider serviceProvider = services.BuildServiceProvider();
    // Enable scope validation
    // using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true);

    _ = serviceProvider.GetRequiredService<Foo>();
}

在上述程式碼中, Foo 是以 singleton 的形式註冊,而且 Bar 範圍限定在表面上看起來是有效的。 但是,請考慮執行 Foo 的。

namespace DependencyInjection.AntiPatterns
{
    public class Foo
    {
        public Foo(Bar bar)
        {
        }
    }
}

Foo物件需要 Bar 物件,因為 Foo 是 singleton, Bar 且已設定範圍-這是設定錯誤的。 同樣地, Foo 它只會具現化一次,而且它會保留 Bar 在其存留期內(長度超過預期的 Bar 範圍存留期)。 您應該考慮透過傳遞 validateScopes: true 給來 BuildServiceProvider(IServiceCollection, Boolean) 驗證範圍。 當您驗證範圍時,您會收到 InvalidOperationException 一個訊息,類似于「無法從單一 ' Foo ' 取用範圍服務 ' Bar '」。

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

範圍服務為 singleton

使用已設定範圍的服務時,如果您不是在現有的範圍內建立範圍,服務會變成 singleton。

static void ScopedServiceBecomesSingleton()
{
    var services = new ServiceCollection();
    services.AddScoped<Bar>();

    using ServiceProvider serviceProvider = services.BuildServiceProvider(validateScopes: true);
    using (IServiceScope scope = serviceProvider.CreateScope())
    {
        // Correctly scoped resolution
        Bar correct = scope.ServiceProvider.GetRequiredService<Bar>();
    }

    // Not within a scope, becomes a singleton
    Bar avoid = serviceProvider.GetRequiredService<Bar>();
}

在上述程式 IServiceScope 代碼中, Bar 是在中的,它是正確的。 反模式是在 Bar 範圍外的抓取,並命名 avoid 變數以顯示不正確的範例抓取。

另請參閱