Sdílet prostřednictvím


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 injektování 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ému vytváření instancí závislých tříd v rámci služeb. Přímé vytvoření instance přiřadí kód ke konkrétní implementaci.
  • Udělejte služby malými, dobře strukturovanými a snadno otestovatelnými.

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

Likvidace služeb

Kontejner je zodpovědný za vyčištění typů, které vytváří, a volá Dispose na IDisposable instancích. Služby získané z kontejneru by nikdy neměly být likvidovány vývojářem. Pokud je typ nebo továrna registrován jako singleton, kontejner automaticky odstraní singleton.

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í jednorázový výrobek je určen k krátkodobé životnosti.

namespace ConsoleDisposable.Example;

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

Předchozí věc na jedno použití je určená pro určitou dobu použití.

namespace ConsoleDisposable.Example;

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

Předchozí jednorázový je určen k singletové ž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 je vyřešena v kořenovém rozsahu (kořenovém kontejneru).
  • Instance by měla být odstraněna před ukončením rozsahu.

Řešení

Použijte vzor továrny k vytvoření instance v oblasti 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í. Použijte IServiceScopeFactory.CreateScope k vytvoření nového IServiceScope. Použijte obor k získání požadovaných služeb IServiceProvider. Zrušte rozsah, 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ří/znovu vytvoří a odstraní IServiceProvider, ale nejedná se o ideální vzorec.
  • Příjem závislosti prostřednictvím DI nevyžaduje, aby příjemce implementoval IDisposable sám. Příjemce IDisposable závislosti by neměl volat Dispose na danou závislost.
  • Používejte rozsahy 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é kontejnerem, 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ího životního cyklu
  • 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 jednoinstanční služby s bezpečným přístupem z více vláken. Pokud má singletonová služba závislost na přechodné službě, může přechodná služba také vyžadovat zabezpečení vláken v závislosti na tom, jak ji singletonová služba 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 bude volán pouze jednou jedním vláknem.

Doporučení

  • async/await a Task založené na službách řešení není podporováno. Vzhledem k tomu, že jazyk C# nepodporuje asynchronní konstruktory, po synchronním vyřešení 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 DI továrny rychlé a synchronní.
  • Nepoužívejte vzor lokátor služby. Například nevyvolávejte, GetService abyste získali instanci služby, když místo toho můžete použít distanci.
  • Další variantou lokátoru služeb, které byste se měli vyhnout, je injektovat továrnu, která řeší závislosti při spuštění. Obě tyto postupy se směšují s inverzí strategií řízení .
  • Vyhněte se volání BuildServiceProvider 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 z tohoto důvodu použijte přetížení, které obsahuje IServiceProvider.
  • Jednorázové přechodné služby jsou zachyceny kontejnerem pro odstranění. To může vést k úniku paměti, pokud je vyřešeno z kontejneru nejvyšší úrovně.
  • Povolte ověřování oboru, pro zajištění, že aplikace nemá singletony, které zachytávají služby s vymezeným oborem. Další informace najdete v tématu Validace rozsahu.

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ého rámce.

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 antivzorů

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ů jsou poznatky získané z vývoje samotných runtime.

Varování

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 bude ve výchozím nastavení uchovávat tyto odkazy a ne Dispose() z nich, dokud kontejner nebude zlikvidován, když se aplikace zastaví, pokud byly vyřešeny z kontejneru, nebo dokud se neukončí rozsah, pokud byly vyřešeny z oboru. To se může změnit na únik paměti, pokud je vyřešeno na úrovni kontejneru.

Anti-pattern: Přechodné uvolnitelné bez odstranění. Nekopírujte!

Ve předchozím anti-příkladu se vytvoří instance 1 000 ExampleDisposable objektů a jsou ukotveny. Nebudou odstraněny, dokud serviceProvider instance nebude odstraněna.

Další informace o ladění úniků paměti naleznete v tématu Ladění úniku paměti v .NET.

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

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

Anti-pattern: Vzájemné zablokování s asynchronním konstruktorem. Nekopírujte!

V předchozím kódu je implementationFactory přiřazen výraz lambda, v němž tělo volá Task<TResult>.Result na metodě vracející 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: Zablokování při použití asynchronní továrny. Nekopírovat!

Další informace o asynchronním poradenství najdete v tématu Asynchronní programování: Důležité informace a rady. Další informace o ladění zablokování naleznete v tématu Jak ladit zablokování v .NET.

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

Omezená závislost

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

Anti-pattern: Vázaná závislost. Nekopírujte!

V předchozím kódu je Foo registrován jako singleton a Bar je vymezený, což se na první pohled zdá být platné. Zvažte však implementaci Foo.

namespace DependencyInjection.AntiPatterns;

public class Foo(Bar bar)
{
}

Objekt Foo vyžaduje objekt Bar, a jelikož Foo je singleton a Bar je vymezený – jedná se o chybnou konfiguraci. V současné podobě by byla instance Foo vytvořena pouze jednou a uchovávala Bar po celou dobu své existence, která je delší než zamýšlená životnost Bar. Měli byste zvážit ověření oborů předáním validateScopes: true do BuildServiceProvider(IServiceCollection, Boolean). Když ověříte obory, dostanete InvalidOperationException se zprávou podobnou této: "Nelze použít vymezenou službu 'Bar' z singletonu 'Foo'.".

Další informace najdete v tématu Validace rozsahu.

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: Služba s vymezeným oborem se stane singletonem. Nekopírujte!

V předchozím kódu se Bar načte uvnitř IServiceScope, což je správné. Anti-patternem je načítání Bar mimo obor a proměnná je pojmenována jako avoid k označení, které ukázkové načítání je nesprávné.

Viz také