Najbolje prakse, savjeti i trikovi za ASP.NET ubrizgavanje jezgre ovisnosti

U ovom ću članku podijeliti svoja iskustva i prijedloge o korištenju Dependency Injection u ASP.NET Core aplikacijama. Motivacija ovih načela je;

  • Učinkovito oblikovanje usluga i njihovih ovisnosti.
  • Sprječavanje problema s više navoja.
  • Sprječavanje curenja memorije.
  • Sprječavanje potencijalnih grešaka.

Ovaj članak pretpostavlja da ste već upoznati sa Dependency Injection i ASP.NET Core na osnovnoj razini. Ako ne, prvo pročitajte dokumentaciju za ubrizgavanje jezgre ASP.NET.

Osnove

Ubrizgavanje konstruktora

Ubrizgavanje konstruktora koristi se za proglašavanje i dobivanje ovisnosti usluge o servisnoj konstrukciji. Primjer:

ProductService javne klase
{
    privatno samo za čitanje IProductRepository _productRepository;
    javna usluga proizvoda (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    javna praznina Izbriši (int id)
    {
        _productRepository.Delete (ID);
    }
}

ProductService ubrizgava IProductRepository kao ovisnost u svoj konstruktor, a zatim ga koristi unutar metode Delete.

Dobre prakse:

  • Definirajte potrebne ovisnosti izričito u servisnom konstruktoru. Dakle, usluga se ne može konstruirati bez svojih ovisnosti.
  • Dodijelite injektiranu ovisnost polju / svojstvu samo za čitanje (da se spriječi slučajno mu se dodijeli druga vrijednost unutar metode).

Injekcija imovine

Standardni spremnik ubrizgavanja ovisnosti ASP.NET Core ne podržava ubrizgavanje svojstava. Ali možete koristiti drugi spremnik koji podržava ubrizgavanje imovine. Primjer:

pomoću Microsofta.Extensions.Logging;
pomoću Microsofta.Extensions.Logging.Abstractions;
imenski prostor MyApp
{
    ProductService javne klase
    {
        javni ILogger  Logger {get; set; }
        privatno samo za čitanje IProductRepository _productRepository;
        javna usluga proizvoda (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger  .Instance;
        }
        javna praznina Izbriši (int id)
        {
            _productRepository.Delete (ID);
            Logger.LogInformation (
                $ "Izbrisao je proizvod s id = {id}");
        }
    }
}

ProductService izjavljuje entitet Logger javnim setterom. Spremnik ubrizgavanja ovisnosti može postaviti Logger ako je dostupan (registriran u DI spremnik prije).

Dobre prakse:

  • Koristite ubrizgavanje svojstava samo za neobavezne ovisnosti. To znači da vaša usluga može ispravno raditi bez pružanja ovih ovisnosti.
  • Koristite Null Object Pattern (kao u ovom primjeru) ako je moguće. U suprotnom, uvijek provjerite ima li nule dok koristite ovisnost.

Lokator usluga

Obrazac lociranja usluga drugi je način dobivanja ovisnosti. Primjer:

ProductService javne klase
{
    privatno samo za čitanje IProductRepository _productRepository;
    privatno samo za čitanje ILogger  _logger;
    javna usluga proizvoda (usluga IServiceProviderProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = uslugaProvider
          .GetService > () ??
            NullLogger  .Instance;
    }
    javna praznina Izbriši (int id)
    {
        _productRepository.Delete (ID);
        _logger.LogInformation ($ "Izbrisan je proizvod s id = {id}");
    }
}

ProductService ubrizgava IServiceProvider i rješava ovisnosti pomoću njega. GetRequiredService izuzeće ako tražena ovisnost nije registrirana prije. S druge strane, GetService u tom slučaju samo vraća nulu.

Kada riješite usluge unutar konstruktora, oni se puštaju nakon puštanja usluge. Dakle, ne zanima vas puštanje / odlaganje usluga riješenih unutar konstruktora (baš poput ubrizgavanja konstruktora i imovine).

Dobre prakse:

  • Ne upotrebljavajte uzorak lokatora usluga kad god je to moguće (ako je vrsta usluge poznata u vrijeme razvoja). Jer to čini ovisnosti implicitnim. To znači da nije moguće lako vidjeti ovisnosti dok stvarate instancu usluge. Ovo je posebno važno za jedinice jedinice gdje se želite rugati nekim ovisnostima usluge.
  • Riješite ovisnosti u servisnom konstruktoru ako je moguće. Rješavanje servisne metode čini vašu aplikaciju složenijom i skloni pogreškama. U sljedećim ćemo odjeljcima obraditi probleme i rješenja.

Vreme trajanja usluge

Postoje tri životna vijeka u ASP.NET ubrizgavanju jezgre:

  1. Privremene usluge stvaraju se svaki put kada se ubrizgavaju ili zatraže.
  2. Opseg usluga kreira se prema opsegu. U web aplikaciji svaki web zahtjev stvara novi odvojeni domet usluge. To znači da se opsežne usluge obično stvaraju po web zahtjevu.
  3. Singleton usluge stvaraju se po DI spremniku. To općenito znači da su stvoreni samo jedan put po aplikaciji, a zatim se koriste tijekom cijelog životnog vijeka aplikacije.

DI kontejner prati sve riješene usluge. Usluge se oslobađaju i odlaze nakon isteka njihovog vijeka trajanja:

  • Ako usluga ima ovisnosti, oni se automatski oslobađaju i zbrinjavaju.
  • Ako usluga implementira sučelje IDisposable, puštanje usluge automatski se poziva na način Dispose.

Dobre prakse:

  • Registrirajte svoje usluge kao prolazne kad god je to moguće. Jer je jednostavno dizajnirati privremene usluge. Uglavnom vas ne zanimaju višestruke navoje i curenje memorije, a znate da servis ima kratak vijek trajanja.
  • Pažljivo koristite opseg životnog vijeka usluge jer može biti teško ako stvorite opseg usluga za djecu ili koristite ove usluge iz ne-web aplikacije.
  • Pažljivo koristite singleton vijek trajanja, od tada se trebate riješiti problema s više navoja i mogućih problema s propuštanjem memorije.
  • Ne ovisite o prolaznoj ili opsežnoj usluzi od singleton usluge. Budući da prolazna usluga postaje singleton instancija kada je singleton usluga ubrizgava i to može stvoriti probleme ako prijelazna usluga nije dizajnirana tako da podržava takav scenarij. ASP.NET Core zadani DI spremnik već baca iznimke u takvim slučajevima.

Rješavanje usluga u tijelu metode

U nekim ćete slučajevima možda trebati riješiti neku drugu uslugu na neki način svoje usluge. U takvim slučajevima osigurajte da uslugu pustite nakon upotrebe. Najbolji način da se to osigura je stvaranje opsega usluge. Primjer:

javna klasa PriceCalculator
{
    privatno samo za čitanje IServiceProvider _serviceProvider;
    javni PriceCalculator (usluga IServiceProviderProvider)
    {
        _serviceProvider = uslugaProvider;
    }
    javni plovak Izračunajte (proizvod proizvoda, ukupan broj,
      Vrsta porezaStrategyServiceType)
    {
        pomoću (var doseg = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) opseg.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            var cijena = proizvod.Price * count;
            povratna cijena + porezStrategy.CalculateTax (cijena);
        }
    }
}

PriceCalculator ubrizgava IServiceProvider u svoj konstruktor i dodjeljuje ga polju. PriceCalculator zatim ga koristi unutar metode izračuna da bi stvorio opseg dječje usluge. Za rješavanje usluga koristi se range.ServiceProvider umjesto ubrizgavane instance _serviceProvider. Stoga se sve usluge koje se rješavaju iz opsega automatski puštaju / odlažu na kraju upotrebnog izraza.

Dobre prakse:

  • Ako rješavate uslugu u tijelu metoda, uvijek stvarajte domet dječje usluge kako biste osigurali da se riješene usluge pravilno izdaju.
  • Ako metoda dobije IServiceProvider kao argument, tada možete izravno riješiti usluge iz nje bez brige o puštanju / odlaganju. Stvaranje / upravljanje opsegom usluge odgovorna je za kôd koji poziva vašu metodu. Slijedeći ovaj princip čini vaš kod čistijim.
  • Ne držite referencu na riješenu uslugu! Inače može prouzrokovati istjecanje memorije i pristupit ćete raspoloživoj usluzi kada kasnije upotrijebite referencu objekta (osim ako je riješena usluga jednokratna).

Singleton usluge

Singleton usluge uglavnom su dizajnirane za održavanje stanja aplikacije. Predmemorija je dobar primjer stanja aplikacije. Primjer:

FileService javne klase
{
    privatno samo za čitanje ConcurrentDictionary  _cache;
    javna FileService ()
    {
        _cache = novi ConcurrentDictionary  ();
    }
    javni bajt [] GetFileContent (string filePath)
    {
        return _cache.GetOrAdd (filePath, _ =>
        {
            vratiti File.ReadAllBytes (filePath);
        });
    }
}

FileService jednostavno predmemorira sadržaj datoteke kako bi smanjio čitanje diska. Ova usluga treba biti registrirana kao singleton. Inače, predmemoriranje neće raditi kako se očekuje.

Dobre prakse:

  • Ako usluga ima državu, trebala bi joj pristupiti na siguran način. Jer svi zahtjevi istodobno koriste istu instancu usluge. Za sigurnost niti sam koristio ConcurrentDictionary umjesto Dictionary.
  • Ne koristite opsežne ili privremene usluge od singleton usluga. Jer, prolazne usluge možda nisu dizajnirane da budu sigurne u nit. Ako ih morate koristiti, vodite računa o više navoja dok koristite ove usluge (na primjer, koristite bravu).
  • Propuštanje memorije obično uzrokuju singleton usluge. Oni se ne puštaju / uklanjaju do kraja prijave. Dakle, ako instanciraju klase (ili ih ubrizgaju), ali ih ne oslobode / raspolože, one će im također ostati u sjećanju do kraja aplikacije. Osigurajte da ih pustite / zbrinete u pravom trenutku. Pogledajte odjeljak Rješavanje usluga u odjeljku Tijelo metoda gore.
  • Ako predmemorirate podatke (sadržaj datoteke u ovom primjeru), trebali biste stvoriti mehanizam za ažuriranje / poništenje predmemoriranih podataka kad se promijeni izvorni izvor podataka (kada se datoteka keširanja promijeni na disku za ovaj primjer).

Opseg usluga

Prvo predviđeni životni vijek čini se dobrim kandidatom za pohranjivanje podataka o internetskim zahtjevima. Budući da ASP.NET Core stvara opseg usluga po web zahtjevu. Dakle, ako registrirate uslugu kao obuhvaćenu, ona se može dijeliti tijekom web zahtjeva. Primjer:

RequestItemsService javne klase
{
    privatni samo za čitanje rječnik  _items;
    javni RequestItemsService ()
    {
        _items = novi rječnik  ();
    }
    skup javnih praznina (naziv niza, vrijednost objekta)
    {
        _items [ime] = vrijednost;
    }
    javni objekt Dohvati (naziv niza)
    {
        return _items [ime];
    }
}

Ako registrirate RequestItemsService kao obuhvaćeno i ubrizgavate ga u dvije različite usluge, tada možete dobiti stavku koja je dodana iz druge usluge jer će dijeliti istu instancu RequestItemsService. To očekujemo od opsežnih usluga.

Ali .. činjenica možda nije uvijek takva. Ako stvorite opseg dječje usluge i riješite RequestItemsService iz podređenog dosega, dobit ćete novu instancu RequestItemsService i ona neće raditi onako kako očekujete. Dakle, opsežna usluga ne znači uvijek instancu po web zahtjevu.

Možda mislite da ne napravite tako očitu pogrešku (rješavanje opsega unutar dječjeg dosega). Ali, ovo nije greška (vrlo redovita upotreba) i slučaj možda nije tako jednostavan. Ako postoji veliki grafikon ovisnosti između vaših usluga, ne možete znati je li netko stvorio dječji domet i riješio uslugu koja ubrizgava drugu uslugu ... koja konačno ubrizgava opseg usluge.

Dobra vježba:

  • Usluga s opsegom može se smatrati optimizacijom u koju u web zahtjev postavlja previše usluga. Stoga će sve ove usluge koristiti jedan primjerak usluge tijekom istog web zahtjeva.
  • Usluge opsega ne moraju biti dizajnirane kao sigurne za niti. Jer, normalno ih treba koristiti jedan web-zahtjev / nit. Ali ... u tom slučaju, ne biste trebali dijeliti opsege usluga između različitih niti!
  • Budite oprezni ako dizajnirate uslugu s opsegom za razmjenu podataka između ostalih usluga u web zahtjevu (objašnjeno gore). Podatke web zahtjeva možete pohraniti unutar HttpContext (ubacite IHttpContextAccessor da biste mu pristupili) što je najsigurniji način da to učinite. Vijek trajanja HttpContext nije obuhvaćen. Zapravo, on uopće nije registriran za DI (zato ga ne ubrizgavate, već umjesto njega ubrizgavate IHttpContextAccessor). Implementacija HttpContextAccessor koristi AsyncLocal za dijeljenje istog HttpContext tijekom web zahtjeva.

Zaključak

Ubrizgavanje ovisnosti na početku se čini jednostavno, ali postoje potencijalni problemi s više navoja i propuštanjem memorije ako ne slijedite neke stroge principe. Podijelio sam neke dobre principe temeljene na vlastitim iskustvima tijekom razvoja ASP.NET okvira kotlova.