Doporučené postupy, tipy a triky pro vstřikování jádra ASP.NET

V tomto článku se budu dělit o své zkušenosti a návrhy týkající se používání Dependency Injection v aplikacích ASP.NET Core. Motivace těchto principů je;

  • Účinně navrhovat služby a jejich závislosti.
  • Prevence problémů s více vlákny.
  • Prevence úniku paměti.
  • Prevence potenciálních chyb.

Tento článek předpokládá, že již znáte Dependency Injection a ASP.NET Core v základní úrovni. Pokud tomu tak není, přečtěte si nejprve dokumentaci k jádru ASP.NET Core Dependency Injection.

Základy

Vstřikování konstruktoru

Injekce konstruktoru se používá k deklaraci a získání závislostí služby na konstrukci služby. Příklad:

veřejná třída ProductService
{
    private readonly IProductRepository _productRepository;
    public ProductService (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    public void Delete (int id)
    {
        _productRepository.Delete (id);
    }
}

ProductService injektuje IProductRepository jako závislost ve svém konstruktoru a poté ji použije uvnitř metody Odstranit.

Osvědčené postupy:

  • Požadované závislosti explicitně definujte ve konstruktoru služby. Službu tedy nelze postavit bez jejích závislostí.
  • Přiřaďte vstřikovanou závislost k poli / vlastnosti pouze pro čtení (abyste zabránili náhodnému přiřazení jiné hodnoty uvnitř metody).

Vstřikování nemovitosti

Standardní vstřikovací kontejner ASP.NET Core nepodporuje vstřikování vlastností. Ale můžete použít další kontejner podporující vstřikování nemovitosti. Příklad:

pomocí Microsoft.Extensions.Logging;
pomocí Microsoft.Extensions.Logging.Abstractions;
jmenný prostor MyApp
{
    veřejná třída ProductService
    {
        public ILogger  Logger {get; soubor; }
        private readonly IProductRepository _productRepository;
        public ProductService (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger  .Instance;
        }
        public void Delete (int id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Odstraněn produkt s id = {id}");
        }
    }
}

ProductService deklaruje vlastnost Logger pomocí veřejného setteru. Závislý vstřikovací kontejner může nastavit Logger, pokud je k dispozici (dříve zaregistrovaný do DI kontejneru).

Osvědčené postupy:

  • Vkládání majetku používejte pouze pro volitelné závislosti. To znamená, že vaše služba může bez těchto závislostí správně fungovat.
  • Pokud je to možné, použijte vzor nulového objektu (jako v tomto příkladu). V opačném případě vždy při používání závislosti zkontrolujte, zda je null.

Vyhledávač služeb

Vzor lokátoru služeb je další způsob získání závislostí. Příklad:

veřejná třída ProductService
{
    private readonly IProductRepository _productRepository;
    private readonly ILogger  _logger;
    public ProductService (IServiceProvider serviceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            NullLogger  .Instance;
    }
    public void Delete (int id)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "Odstraněn produkt s id = {id}");
    }
}

ProductService vstřikuje IServiceProvider a řeší závislosti pomocí něj. GetRequiredService vyvolá výjimku, pokud požadovaná závislost nebyla dříve zaregistrována. Na druhou stranu GetService v tomto případě vrací null.

Když vyřešíte služby uvnitř konstruktoru, budou uvolněny při uvolnění služby. Nezajímá vás tedy uvolňování / likvidace služeb vyřešených uvnitř konstruktoru (stejně jako injekce konstruktoru a majetku).

Osvědčené postupy:

  • Vzor lokátoru služeb nepoužívejte všude, kde je to možné (pokud je typ služby znám v době vývoje). Protože to dělá závislosti implicitní. To znamená, že při vytváření instance služby není možné snadno zjistit závislosti. To je zvláště důležité pro testy jednotek, kde budete možná chtít zesměšňovat některé závislosti služby.
  • Pokud je to možné, vyřešte závislosti ve konstruktoru služeb. Řešení v servisní metodě způsobí, že je aplikace komplikovanější a náchylnější k chybám. V následujících sekcích se budu věnovat problémům a řešením.

Doba životnosti

V ASP.NET Core Dependency Injection jsou tři servisní doby:

  1. Přechodné služby jsou vytvářeny pokaždé, když jsou injektovány nebo vyžádány.
  2. Rozsahové služby jsou vytvářeny podle rozsahu. Ve webové aplikaci vytvoří každý webový požadavek nový samostatný rozsah služeb. To znamená, že rozsahové služby jsou obecně vytvářeny na webovou žádost.
  3. Singletonové služby jsou vytvářeny na DI kontejner. To obecně znamená, že jsou vytvořeny pouze jednou pro každou aplikaci a poté použity po celou dobu životnosti aplikace.

DI kontejner sleduje všechny vyřešené služby. Služby jsou uvolňovány a likvidovány po skončení jejich životnosti:

  • Pokud má služba závislosti, automaticky se uvolní a zlikviduje.
  • Pokud služba implementuje rozhraní IDisposable, metoda Dispose se automaticky volá po uvolnění služby.

Osvědčené postupy:

  • Zaregistrujte své služby, pokud je to možné, jako přechodné. Protože je snadné navrhnout přechodné služby. Obecně se nestaráte o vícevláknové a nevracení paměti a víte, že služba má krátký život.
  • Používejte celý rozsah služeb opatrně, protože to může být složité, pokud vytvoříte podřízené rozsahy služeb nebo používáte tyto služby z jiné než webové aplikace.
  • Používejte singleton celý život opatrně od té doby musíte vypořádat s více podprocesů a potenciální problémy s únikem paměti.
  • Nespoléhejte na přechodnou nebo sledovanou službu ze služby singleton. Protože se přechodná služba stává instancí singletonu, když ji vstřikuje singletonová služba, což může způsobit problémy, pokud přechodná služba není navržena tak, aby podporovala takový scénář. Výchozí DI kontejner ASP.NET Core již v takových případech vyvolává výjimky.

Řešení služeb v těle metody

V některých případech budete možná muset vyřešit jinou službu v metodě své služby. V takových případech se ujistěte, že službu uvolníte po použití. Nejlepší způsob, jak to zajistit, je vytvořit rozsah služeb. Příklad:

veřejná třída PriceCalculator
{
    private readonly IServiceProvider _serviceProvider;
    veřejný PriceCalculator (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    veřejný float Calculate (produkt produktu, int count,
      Zadejte taxStrategyServiceType)
    {
        using (var obor = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) range.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            var price = product.Price * count;
            vrácená cena + daňStrategy.CalculateTax (cena);
        }
    }
}

PriceCalculator vloží IServiceProvider do svého konstruktoru a přiřadí jej k poli. PriceCalculator ji poté použije uvnitř metody Calculate k vytvoření oboru podřízených služeb. K vyřešení služeb používá obor působnosti.ServiceProvider, namísto injektované instance _serviceProvider. Všechny služby vyřešené z rozsahu jsou tedy automaticky uvolněny / zlikvidovány na konci prohlášení o používání.

Osvědčené postupy:

  • Pokud řešíte službu v těle metody, vždy vytvořte obor podřízené služby, abyste zajistili správné uvolnění vyřešených služeb.
  • Pokud metoda získá jako argument IServiceProvider, můžete z něj přímo vyřešit služby bez péče o uvolnění / likvidaci. Za vytvoření / správu rozsahu služeb odpovídá kód, který volá vaši metodu. Díky tomuto principu bude váš kód čistší.
  • Nedržte odkaz na vyřešenou službu! V opačném případě může dojít k úniku paměti a vy budete mít přístup k likvidované službě, když později použijete odkaz na objekt (pokud není vyřešená služba singleton).

Singleton Services

Služby Singleton jsou obecně navrženy tak, aby udržovaly stav aplikace. Mezipaměť je dobrým příkladem stavů aplikace. Příklad:

veřejná třída FileService
{
    private readonly ConcurrentDictionary <řetězec, byte []> _cache;
    veřejné FileService ()
    {
        _cache = new ConcurrentDictionary <řetězec, byte []> ();
    }
    public byte [] GetFileContent (řetězec filePath)
    {
        return _cache.GetOrAdd (filePath, _ =>
        {
            return File.ReadAllBytes (filePath);
        });
    }
}

FileService jednoduše ukládá do mezipaměti obsah souborů, aby se snížilo čtení na disku. Tato služba by měla být zaregistrována jako singleton. V opačném případě nebude ukládání do mezipaměti fungovat podle očekávání.

Osvědčené postupy:

  • Pokud služba udržuje stav, měla by k tomuto stavu přistupovat způsobem bezpečným pro vlákna. Protože všechny požadavky současně používají stejnou instanci služby. Použil jsem ConcurrentDictionary místo Dictionary pro zajištění bezpečnosti vlákna.
  • Nepoužívejte zaměřené nebo přechodné služby ze služeb singleton. Protože přechodné služby nemusí být navrženy tak, aby byly bezpečné pro podprocesy. Pokud je musíte používat, při používání těchto služeb se postarejte o více vláken (například použijte zámek).
  • Únik paměti je obvykle způsoben singletonovými službami. Nejsou uvolňovány / likvidovány až do konce aplikace. Pokud tedy vytvoří instanci tříd (nebo injektují), ale ne je uvolní / zlikvidují, zůstanou v paměti až do konce aplikace. Ujistěte se, že je uvolníte / zlikvidujete ve správný čas. Viz část Řešení služeb v části Metoda výše.
  • Pokud ukládáte data do mezipaměti (obsah souboru v tomto příkladu), měli byste vytvořit mechanismus pro aktualizaci / zneplatnění dat uložených v mezipaměti, když se změní původní zdroj dat (v tomto příkladu se změní soubor v mezipaměti na disku).

Zaměřené služby

Předpokládaná životnost se nejprve jeví jako dobrý kandidát k ukládání dat na webový požadavek. Protože ASP.NET Core vytváří rozsah služeb pro každý webový požadavek. Pokud tedy zaregistrujete službu jako oborovou, lze ji během webového požadavku sdílet. Příklad:

veřejná třída RequestItemsService
{
    private readonly Dictionary <řetězec, objekt> _items;
    veřejné RequestItemsService ()
    {
        _items = new Dictionary  ();
    }
    sada public void (název řetězce, hodnota objektu)
    {
        _items [jméno] = hodnota;
    }
    veřejný objekt Get (název řetězce)
    {
        return _items [jméno];
    }
}

Pokud zaregistrujete RequestItemsService jako obor a vložíte ji do dvou různých služeb, můžete získat položku, která je přidána z jiné služby, protože budou sdílet stejnou instanci RequestItemsService. To je to, co očekáváme od poskytovaných služeb.

Ale .. skutečnost nemusí být vždy taková. Pokud vytvoříte obor podřízené služby a vyřešíte RequestItemsService z podřízeného oboru, získáte novou instanci RequestItemsService a nebude fungovat podle očekávání. Rozsahová služba tedy neznamená vždy instanci na webový požadavek.

Můžete si myslet, že neděláte takovou zjevnou chybu (vyřešení rozsahu v rámci rozsahu dítěte). To však není chyba (velmi pravidelné používání) a případ nemusí být tak jednoduchý. Pokud mezi vašimi službami existuje velký graf závislosti, nemůžete vědět, zda někdo vytvořil podřízený obor a vyřešil službu, která vstřikuje jinou službu ... která nakonec vstřikuje službu s rozsahem.

Dobrý trénink:

  • Rozsahovou službu lze považovat za optimalizaci, pokud je do webové žádosti vloženo příliš mnoho služeb. Všechny tyto služby tedy budou používat jednu instanci služby během stejné webové žádosti.
  • Zaměřené služby nemusí být navrženy jako podprocesové. Protože by je měl běžně používat jediný webový požadavek / vlákno. Ale… v tom případě byste neměli sdílet rozsahy služeb mezi různými vlákny!
  • Buďte opatrní, pokud navrhujete oborovanou službu pro sdílení dat mezi ostatními službami ve webové žádosti (vysvětleno výše). Můžete uložit data na webový požadavek uvnitř HttpContext (vstoupit do IHttpContextAccessor pro přístup), což je bezpečnější způsob, jak toho dosáhnout. Životnost HttpContext není stanovena. Ve skutečnosti není zaregistrován u DI vůbec (proto jej nevstřikujete, ale místo toho injikujte IHttpContextAccessor). Implementace HttpContextAccessor používá AsyncLocal ke sdílení stejného HttpContext během webového požadavku.

Závěr

Používání injekce závislosti se nejprve jeví jako jednoduché, ale pokud nedodržíte některé přísné zásady, může dojít k problémům s více vlákny a únikem paměti. Při vývoji rámce ASP.NET Boilerplate jsem sdílel několik dobrých principů na základě vlastních zkušeností.