Najlepšie postupy, tipy a triky pre vstrekovanie jadra ASP.NET

V tomto článku sa budem deliť o svoje skúsenosti a návrhy týkajúce sa používania závislých injekcií v jadrových aplikáciách ASP.NET. Motivácia týchto princípov je;

  • Efektívne navrhovanie služieb a ich závislosti.
  • Predchádzanie problémom s viacerými vláknami.
  • Predchádzanie únikom pamäte.
  • Prevencia potenciálnych chýb.

V tomto článku sa predpokladá, že ste už oboznámení s aplikáciou Dependency Injection a ASP.NET Core na základnej úrovni. Ak nie, prečítajte si najskôr dokumentáciu základnej injekcie závislosti ASP.NET.

základy

Injekcia konštruktora

Injekcia konštruktora sa používa na deklarovanie a získanie závislostí služby od konštrukcie služby. Príklad:

verejná trieda ProductService
{
    súkromné ​​iba na čítanie IProductRepository _productRepository;
    verejné ProductService (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    public void Delete (int id)
    {
        _productRepository.Delete (id);
    }
}

ProductService injektuje IProductRepository ako závislosť vo svojom konštruktore a potom ju použije v metóde Delete.

Osvedčené postupy:

  • Požadované závislosti explicitne definujte v konštruktore služby. Služba teda nemôže byť postavená bez jej závislostí.
  • Priraďte závislosť od injekcie k poľu / vlastnosti iba na čítanie (aby ste predišli náhodnému priradeniu inej hodnoty v rámci metódy).

Vstrekovanie nehnuteľnosti

Štandardný zásobník injekcie závislosti ASP.NET Core nepodporuje vstrekovanie majetku. Môžete však použiť ďalší kontajner podporujúci vstrekovanie majetku. Príklad:

pomocou Microsoft.Extensions.Logging;
pomocou Microsoft.Extensions.Logging.Abstractions;
namespace MyApp
{
    verejná trieda ProductService
    {
        public ILogger  Logger {get; nastavený; }
        súkromné ​​iba na čítanie IProductRepository _productRepository;
        verejné ProductService (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger  .Instance;
        }
        public void Delete (int id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Odstránený produkt s id = {id}");
        }
    }
}

ProductService deklaruje vlastnosť Logger pomocou verejného nastavovača. Závislý injekčný zásobník môže nastaviť Logger, ak je k dispozícii (predtým zaregistrovaný do DI kontajnera).

Osvedčené postupy:

  • Vkladanie majetku používajte iba pre voliteľné závislosti. To znamená, že vaša služba môže bez týchto závislostí správne fungovať.
  • Ak je to možné, použite vzor nulového objektu (podobne ako v tomto príklade). V opačnom prípade vždy skontrolujte nulovú hodnotu počas používania závislosti.

Vyhľadávač služieb

Vzor lokátora služieb je ďalší spôsob získania závislosti. Príklad:

verejná trieda ProductService
{
    súkromné ​​iba na čítanie IProductRepository _productRepository;
    súkromný čitateľ ILogger  _logger;
    public ProductService (IServiceProvider serviceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            NullLogger  .Instance;
    }
    public void Delete (int id)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "Odstránený produkt s id = {id}");
    }
}

ProductService aplikuje IServiceProvider a rieši závislosti pomocou neho. GetRequiredService vyvolá výnimku, ak požadovaná závislosť nebola predtým zaregistrovaná. Na druhú stranu, GetService len vráti null v tomto prípade.

Keď vyriešite služby vo vnútri konštruktora, uvoľnia sa pri uvoľnení služby. Takže vám nezáleží na uvoľňovaní / likvidácii služieb vyriešených vo vnútri konštruktora (rovnako ako pri vkladaní konštruktora a majetku).

Osvedčené postupy:

  • Vzor vyhľadávača služby nepoužívajte všade, kde je to možné (ak je typ služby známy v čase vývoja). Pretože to robí závislosť implicitnou. To znamená, že pri vytváraní inštancie služby nie je možné ľahko zistiť závislosť. Toto je obzvlášť dôležité pri testoch jednotiek, kde by ste mohli chcieť zosmiešňovať niektoré závislosti služby.
  • Ak je to možné, vyriešte závislosti v konštruktéri služby. Riešenie v servisnej metóde robí vašu aplikáciu komplikovanejšou a náchylnejšou na chyby. V nasledujúcich častiach sa venujem problémom a riešeniam.

Časy životnosti

V ASP.NET Core Dependency Injection sú tri životnosti služby:

  1. Prechodné služby sa vytvárajú zakaždým, keď sa im podajú injekcie alebo žiadosti.
  2. Rozsahové služby sa vytvárajú podľa rozsahu. Vo webovej aplikácii každá webová požiadavka vytvorí nový oddelený rozsah služieb. To znamená, že služby s rozsahom sa spravidla vytvárajú na webovú žiadosť.
  3. Služby Singleton sa vytvárajú pre každý kontajner DI. To vo všeobecnosti znamená, že sa vytvoria iba raz pre každú aplikáciu a potom sa použijú po celú dobu životnosti aplikácie.

Kontajner DI sleduje všetky vyriešené služby. Služby sa uvoľňujú a likvidujú po skončení ich životnosti:

  • Ak má služba závislosť, automaticky sa uvoľní a zlikviduje.
  • Ak služba implementuje rozhranie IDisposable, metóda Dispose sa automaticky volá po uvoľnení služby.

Osvedčené postupy:

  • Zaregistrujte svoje služby podľa možnosti ako prechodné. Pretože je jednoduché navrhnúť prechodné služby. Zvyčajne sa nestaráte o viaczávitové vlákna a úniky pamäte a viete, že služba má krátku životnosť.
  • Používajte službu s obmedzeným dosahom opatrne, pretože pri vytváraní rozsahu služieb pre deti alebo pri využívaní týchto služieb z iných ako webových aplikácií môže byť zložité.
  • Používajte singleton lifetime opatrne od tej doby musíte riešiť viacvláknové a potenciálne problémy s únikom pamäte.
  • Nespoliehajte sa na prechodnú alebo sledovanú službu zo služby singleton. Pretože prechodná služba sa stane samostatnou inštanciou, keď ju vstrekne služba singleton, a to môže spôsobiť problémy, ak prechodná služba nie je navrhnutá na podporu takéhoto scenára. V týchto prípadoch predvolený DI kontajner ASP.NET Core už vyvoláva výnimky.

Riešenie služieb v tele metódy

V niektorých prípadoch bude možno potrebné v rámci vašej služby vyriešiť inú službu. V takom prípade sa uistite, že službu uvoľníte po použití. Najlepším spôsobom, ako to zabezpečiť, je vytvoriť rozsah služieb. Príklad:

verejná cenaCalculator
{
    private readonly IServiceProvider _serviceProvider;
    verejný kalkulátor ceny (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    verejný float Calculate (produkt produktu, int count,
      Zadajte taxStrategyServiceType)
    {
        použitie (var rozsah = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) range.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            var price = product.Price * count;
            cena vrátenia + daňStrategy.CalculateTax (cena);
        }
    }
}

PriceCalculator vloží IServiceProvider do svojho konštruktora a priradí ho k poľu. PriceCalculator ho potom použije v metóde Calculate na vytvorenie rozsahu dcérskej služby. Na vyriešenie služieb používa rozsah.ServiceProvider namiesto injektovanej inštancie _serviceProvider. Všetky služby vyriešené z rozsahu sa teda automaticky uvoľnia / zlikvidujú na konci vyhlásenia o používaní.

Osvedčené postupy:

  • Ak riešite službu v tele metódy, vždy vytvorte rozsah podriadenej služby, aby ste sa uistili, že vyriešené služby sú správne uvoľnené.
  • Ak metóda získa ako argument IServiceProvider, môžete z neho priamo rozlíšiť služby bez toho, aby ste sa starali o uvoľnenie / zneškodnenie. Za vytvorenie / správu rozsahu služieb je zodpovedný kód, ktorý volá vašu metódu. Vďaka tomuto princípu je váš kód čistejší.
  • Nedržte odkaz na vyriešenú službu! V opačnom prípade to môže spôsobiť únik pamäte a vy budete mať prístup k likvidovanej službe, keď neskôr použijete odkaz na objekt (pokiaľ nie je vyriešená služba singleton).

Singleton Services

Služby Singleton sú všeobecne navrhnuté tak, aby udržali stav aplikácie. Vyrovnávacia pamäť je dobrým príkladom stavov aplikácií. Príklad:

verejná trieda FileService
{
    private readonly ConcurrentDictionary  _cache;
    verejné FileService ()
    {
        _cache = new ConcurrentDictionary  ();
    }
    public byte [] GetFileContent (string filePath)
    {
        return _cache.GetOrAdd (filePath, _ =>
        {
            return File.ReadAllBytes (filePath);
        });
    }
}

FileService jednoducho ukladá obsah súboru do vyrovnávacej pamäte, aby sa znížilo čítanie disku. Táto služba by sa mala zaregistrovať ako singleton. V opačnom prípade nebude ukladanie do pamäte cache fungovať podľa očakávania.

Osvedčené postupy:

  • Ak je služba v stave, mala by k nemu pristupovať bezpečným spôsobom. Pretože všetky žiadosti súčasne používajú rovnakú inštanciu služby. Na zaistenie bezpečnosti vlákna som použil namiesto slovníka funkciu ConcurrentDictionary.
  • Nepoužívajte služby s obmedzeným dosahom alebo prechodné služby zo služieb singleton. Pretože prechodné služby nemusia byť navrhnuté tak, aby boli bezpečné. Ak ich musíte používať, pri používaní týchto služieb sa postarajte o postupovanie viacerých vlákien (napríklad použite zámok).
  • Úniky pamäte sú zvyčajne spôsobené službami singleton. Až do konca aplikácie sa neuvoľňujú / likvidujú. Ak teda vytvoria inštancie tried (alebo vstreknú), ale ich neuvoľnia / zlikvidujú, zostanú v pamäti až do konca aplikácie. Uistite sa, že ich v správny čas uvoľníte alebo zlikvidujete. Pozrite si časť Vyriešenie služieb v časti Metóda vyššie.
  • Ak ukladáte údaje do vyrovnávacej pamäte (obsah súboru v tomto príklade), mali by ste vytvoriť mechanizmus na aktualizáciu / zneplatnenie údajov uložených v pamäti pri zmene pôvodného zdroja údajov (ak sa tento príklad zmení na disku na disku).

Služby s rozsahom

Životnosť s maximálnym dosahom sa zdá byť dobrým kandidátom na uloženie údajov na webovú žiadosť. Pretože ASP.NET Core vytvára rozsah služieb pre každú webovú požiadavku. Ak teda zaregistrujete službu v rozsahu, môže sa zdieľať počas webovej požiadavky. Príklad:

verejná trieda RequestItemsService
{
    private readonly Dictionary  _items;
    verejné RequestItemsService ()
    {
        _items = new Dictionary  ();
    }
    sada public void (názov reťazca, hodnota objektu)
    {
        _items [name] = value;
    }
    verejný objekt Get (názov reťazca)
    {
        return _items [meno];
    }
}

Ak zaregistrujete RequestItemsService ako rozsah a vložíte ju do dvoch rôznych služieb, môžete získať položku, ktorá sa pridá z inej služby, pretože budú zdieľať rovnakú inštanciu RequestItemsService. To je to, čo očakávame od poskytovaných služieb.

Ale .. skutočnosť nemusí byť vždy taká. Ak vytvoríte rozsah podriadených služieb a vyriešite RequestItemsService z podriadeného rozsahu, dostanete novú inštanciu RequestItemsService a nebude fungovať podľa očakávania. Rozsahová služba teda nemusí vždy znamenať inštanciu na webovú požiadavku.

Môžete si myslieť, že neurobíte takúto zjavnú chybu (vyriešenie rozsahu v rozsahu dieťaťa). To však nie je chyba (veľmi pravidelné používanie) a prípad nemusí byť taký jednoduchý. Ak medzi vašimi službami existuje veľký graf závislosti, nemôžete vedieť, či niekto vytvoril rozsah pre deti a vyriešil službu, ktorá vstrekuje inú službu ... ktorá napokon zavedie rozsahovú službu.

Dobre cvicenie:

  • Službu s rozsahom možno považovať za optimalizáciu, ak ju do webovej žiadosti vloží príliš veľa služieb. Všetky tieto služby teda použijú jednu inštanciu služby počas tej istej webovej požiadavky.
  • Rozsahové služby nemusia byť navrhnuté ako bezpečné. Zvyčajne by ich preto mala používať jediná webová žiadosť / vlákno. Ale ... v takom prípade by ste nemali zdieľať rozsahy služieb medzi rôznymi vláknami!
  • Buďte opatrní, ak navrhujete službu s rozsahom na zdieľanie údajov medzi ostatnými službami vo webovej žiadosti (vysvetlené vyššie). Dáta na webovú žiadosť môžete ukladať vo vnútri HttpContext (na prístup do aplikácie injektujte IHttpContextAccessor), čo je bezpečnejší spôsob. Životnosť HttpContext nie je obmedzená. V skutočnosti nie je zaregistrovaný u DI vôbec (preto ho nevstrekujete, ale namiesto toho injikujte IHttpContextAccessor). Implementácia HttpContextAccessor používa AsyncLocal na zdieľanie toho istého HttpContext počas webovej požiadavky.

záver

Aplikácia závislosti je spočiatku jednoduchá, ale ak nerešpektujete niektoré prísne princípy, môžu sa vyskytnúť problémy s viacerými vláknami a únikom pamäte. Pri vývoji rámca ASP.NET Boilerplate som zdieľal niekoľko dobrých princípov.