Pokyny pro injektáž závislostí

Tento článek obsahuje obecné pokyny a osvědčené postupy pro implementaci injektáže závislostí v aplikacích .NET.

Návrh služeb pro injektáž závislostí

Při navrhování služeb pro injektáž závislostí:

  • Vyhněte se stavovým, statickým třídám a členům. Vyhněte se vytváření globálního stavu návrhem aplikací tak, aby místo toho používaly jednoúčelové služby.
  • Vyhněte se přímé instanci závislých tříd v rámci služeb. Přímé vytvoření instance přiřadí kód ke konkrétní implementaci.
  • Zpřístupnit služby malými, dobře faktorovanými a snadno otestovanými.

Pokud má třída mnoho vloženého závislostí, může to být znamení, že třída má příliš mnoho zodpovědností a porušuje zásadu SRP (Single Responsibility Principle). Pokuste se refaktorovat třídu přesunutím některých svých zodpovědností do nových tříd.

Likvidace služeb

Kontejner zodpovídá za vyčištění typů, které vytváří, a volá DisposeIDisposable instance. Služby vyřešené z kontejneru by nikdy neměly být uvolněny vývojářem. Pokud je typ nebo továrna registrována jako singleton, kontejner automaticky odstraní jednoton.

V následujícím příkladu jsou služby vytvořeny kontejnerem služby a automaticky odstraněny:

namespace ConsoleDisposable.Example;

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

Předchozí jedno použití je určeno k přechodné životnosti.

namespace ConsoleDisposable.Example;

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

Předchozí jedno použití má mít vymezenou dobu života.

namespace ConsoleDisposable.Example;

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

Předchozí jedno použití je určeno k jednotonové životnosti.

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

Po spuštění konzoly ladění se zobrazí následující ukázkový výstup:

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

Služby, které se nevytvořily kontejnerem služby

Uvažujte následující kód:

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

V předchozím kódu:

  • Instance ExampleService není vytvořena kontejnerem služby.
  • Architektura nelikviduje služby automaticky.
  • Vývojář zodpovídá za likvidaci služeb.

Pokyny pro IDisposable pro přechodné a sdílené instance

Přechodná, omezená životnost

Scénář

Aplikace vyžaduje IDisposable instanci s přechodnou životností pro některý z následujících scénářů:

  • Instance se přeloží v kořenovém oboru (kořenovém kontejneru).
  • Instance by měla být odstraněna před ukončením oboru.

Řešení

Pomocí vzoru továrny vytvořte instanci mimo nadřazený obor. V takovém případě by aplikace měla obecně metodu Create , která volá přímo konstruktor konečného typu. Pokud konečný typ obsahuje další závislosti, může továrna:

Sdílená instance, omezená životnost

Scénář

Aplikace vyžaduje sdílenou IDisposable instanci napříč několika službami, ale IDisposable instance by měla mít omezenou životnost.

Řešení

Zaregistrujte instanci s vymezenou životností. Slouží IServiceScopeFactory.CreateScope k vytvoření nového IServiceScope. K získání požadovaných IServiceProvider služeb použijte obor. Vyřaďte obor, pokud už ho nepotřebujete.

Obecné IDisposable pokyny

  • Nezaregistrujte IDisposable instance s přechodnou životností. Místo toho použijte vzor továrny.
  • Nevyřešujte IDisposable instance s přechodnou nebo vymezenou životností v kořenovém oboru. Jedinou výjimkou je, když aplikace vytvoří nebo znovu vytvoří a odstraní IServiceProvider, ale nejedná se o ideální vzor.
  • IDisposable Příjem závislosti prostřednictvím DI nevyžaduje, aby se příjemce implementovala IDisposable sama. Příjemce IDisposable závislosti by neměl volat Dispose na danou závislost.
  • Rozsahy slouží k řízení životnosti služeb. Obory nejsou hierarchické a mezi obory neexistuje žádné zvláštní propojení.

Další informace o vyčištění prostředků naleznete v tématu Implementace Dispose metody nebo Implementace DisposeAsync metody. Kromě toho zvažte jednorázové přechodné služby zachycené scénářem kontejneru , protože souvisí s vyčištěním prostředků.

Nahrazení výchozího kontejneru služby

Integrovaný kontejner služby je navržený tak, aby sloužil potřebám architektury a většiny uživatelských aplikací. Doporučujeme používat integrovaný kontejner, pokud nepotřebujete konkrétní funkci, kterou nepodporuje, například:

  • Injektáž vlastností
  • Injektáž založená na názvu (jenom .NET 7 a starší verze). Další informace najdete v tématu Služby s klíči.)
  • Podřízené kontejnery
  • Správa vlastní životnosti
  • Func<T> podpora opožděné inicializace
  • Registrace na základě konvence

Následující kontejnery třetích stran je možné použít s aplikacemi ASP.NET Core:

Bezpečnost vlákna

Vytvořte jednoúčelové služby bezpečné pro přístup z více vláken. Pokud má jednoúčelová služba závislost na přechodné službě, může přechodná služba také vyžadovat bezpečnost vláken v závislosti na tom, jak ji singleton používá.

Metoda továrny jednoúčelové služby, například druhý argument AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), nemusí být bezpečný pro přístup z více vláken. Stejně jako konstruktor typu (static) je zaručeno, že se bude volat pouze jednou jedním vláknem.

Doporučení

  • async/await a Task na základě řešení služeb se nepodporuje. Vzhledem k tomu, že jazyk C# nepodporuje asynchronní konstruktory, po synchronním překladu služby používejte asynchronní metody.
  • Vyhněte se ukládání dat a konfigurace přímo v kontejneru služby. Nákupní košík uživatele by například neměl být obvykle přidán do kontejneru služeb. Konfigurace by měla používat vzor možností. Podobně se vyhněte objektům "držitel dat", které existují pouze pro povolení přístupu k jinému objektu. Je lepší požádat o skutečnou položku prostřednictvím DI.
  • Vyhněte se statickému přístupu ke službám. Vyhněte se například zachycení IApplicationBuilder.ApplicationServices jako statického pole nebo vlastnosti pro použití jinde.
  • Udržujte továrny DI rychle a synchronní.
  • Nepoužívejte vzor lokátoru služby. Například nevyvolávejte, GetService abyste získali instanci služby, když místo toho můžete použít distanci.
  • Jinou variantou lokátoru služby, která se vyhne, je vložení továrny, která řeší závislosti za běhu. Obě tyto postupy se směšují s inverzí strategií řízení .
  • Vyhněte se BuildServiceProvider volání při konfiguraci služeb. Volání BuildServiceProvider obvykle nastane, když vývojář chce vyřešit službu při registraci jiné služby. Místo toho použijte přetížení, které obsahuje IServiceProvider z tohoto důvodu.
  • Jednorázové přechodné služby jsou zachyceny kontejnerem pro odstranění. To se může při řešení z kontejneru nejvyšší úrovně změnit na nevrácenou paměť.
  • Povolte ověřování oboru, abyste měli jistotu, že aplikace nemá jednotony, které zachycují vymezené služby. Další informace najdete v tématu Ověření oboru.

Stejně jako u všech sad doporučení můžete narazit na situace, kdy se vyžaduje ignorování doporučení. Výjimky jsou vzácné, většinou zvláštní případy v rámci samotné architektury.

Di je alternativou ke vzorům statického nebo globálního přístupu k objektům. Pokud ho kombinujete se statickým přístupem k objektům, možná nebudete moct realizovat výhody DI.

Příklady vzorů proti vzorům

Kromě pokynů v tomto článku existuje několik anti-vzorů , kterým byste se měli vyhnout. Některé z těchto anti-vzorů se učí od vývoje samotných modulů runtime.

Upozorňující

Jedná se o ukázkové anti-vzory, nekopírujte kód, nepoužívejte tyto vzory a vyhněte se těmto vzorům za všechny náklady.

Jednorázové přechodné služby zachycené kontejnerem

Když zaregistrujete přechodné služby, které implementují IDisposable, kontejner DI ve výchozím nastavení bude uchovávat tyto odkazy, a ne Dispose() z nich, dokud se kontejner nezastaví, pokud se aplikace zastaví, pokud byly vyřešeny z kontejneru, nebo dokud se rozsah neodloží, pokud byly vyřešeny z oboru. To se může změnit na nevracení paměti, pokud je vyřešeno z úrovně kontejneru.

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

V předchozím anti-vzoru se vytvoří instance 1 000 ExampleDisposable objektů a root. Nebudou odstraněny, dokud serviceProvider instance nebude odstraněna.

Další informace o ladění nevracení paměti naleznete v tématu Ladění nevracení paměti v .NET.

Asynchronní továrny DI můžou způsobit zablokování

Termín "DI factory" odkazuje na metody přetížení, které existují při volání Add{LIFETIME}. Existují přetížení, které přijímají, kde Func<IServiceProvider, T>T je služba registrována, a parametr je pojmenován implementationFactory. Dá implementationFactory se zadat jako výraz lambda, místní funkce nebo metoda. Pokud je továrna asynchronní a použijete Task<TResult>.Resultji, způsobí to zablokování.

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

V předchozím kódu je uveden výraz lambda, implementationFactory kde tělo volá Task<TResult>.Result návratovou metodu Task<Bar> . To způsobí zablokování. Metoda GetBarAsync jednoduše emuluje asynchronní pracovní operaci s Task.Delay, a pak volá GetRequiredService<T>(IServiceProvider).

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

Další informace o asynchronních doprovodných materiálech najdete v tématu Asynchronní programování: Důležité informace a rady. Další informace o ladění zablokování naleznete v tématu Ladění vzájemné zablokování v .NET.

Když spouštíte tento anti-pattern a dojde k zablokování, můžete zobrazit dvě vlákna čekající z okna Paralelní zásobníky sady Visual Studio. Další informace najdete v tématu Zobrazení vláken a úloh v okně Paralelní zásobníky.

Závislost v závislosti na závislosti

Pojem "závislost v držení" byl vytvořen Markem Seemannem a odkazuje na nesprávnou konfiguraci životnosti služeb, kde delší služba uchovává kratší dobu provozu.

Anti-pattern: Captive dependency. Do not copy!

V předchozím kódu Foo je registrován jako singleton a Bar je vymezen - který na povrchu vypadá jako platný. Zvažte však provádění Foo.

namespace DependencyInjection.AntiPatterns;

public class Foo(Bar bar)
{
}

Objekt Foo vyžaduje Bar objekt, a protože Foo je jednoúčelový a Bar je vymezený – jedná se o chybnou konfiguraci. Jak je tomu tak, Foo vytvoří se instance pouze jednou a bude po celou Bar dobu života, která je delší než zamýšlená doba života Bar. Měli byste zvážit ověření oborů předáním validateScopes: true souboru BuildServiceProvider(IServiceCollection, Boolean). Když ověříte obory, zobrazí InvalidOperationException se zpráva podobná tomu, že nejde využívat vymezenou službu Bar z singletonu Foo.

Další informace najdete v tématu Ověření oboru.

Služba s vymezeným oborem jako singleton

Pokud při použití služeb s vymezeným oborem nevytvářete obor nebo v rámci existujícího oboru , stane se služba jediným oborem.

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

V předchozím kódu Bar se načte v objektu IServiceScope, který je správný. Anti-pattern je načtení Bar mimo obor a proměnná je pojmenovaná avoid tak, aby ukázala, která ukázková načtení není správná.

Viz také