Najboljše prakse, nasveti in triki ASP.NET Core Dependency Injection Best Practices, Nasveti in triki

V tem članku bom delil svoje izkušnje in predloge o uporabi Dependency Injection v aplikacijah ASP.NET Core. Motivacija teh načel je;

  • Učinkovito oblikovanje storitev in njihovih odvisnosti.
  • Preprečevanje težav z več nitmi.
  • Preprečevanje puščanja spomina
  • Preprečevanje morebitnih napak.

Ta članek predvideva, da ste že seznanjeni z vbrizgavanjem odvisnosti in ASP.NET Core na osnovni ravni. Če ne, najprej preberite dokumentacijo za vbrizgavanje osrednje odvisnosti ASP.NET.

Osnove

Vbrizgavanje konstruktorja

Konstrukcijsko brizganje se uporablja za razglasitev in pridobitev odvisnosti storitve od storitvene konstrukcije. Primer:

ProductService v javnem razredu
{
    zasebno samo za branje IProductRepository _productRepository;
    javna storitev izdelka (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    javna ničnost Izbriši (int id)
    {
        _productRepository.Delete (id);
    }
}

ProductService vbrizga IProductRepository kot odvisnost v svoj konstruktor, nato pa ga uporabi znotraj metode Delete.

Dobre prakse:

  • V konstruktorju storitev izrecno določite zahtevane odvisnosti. Tako storitve ni mogoče zgraditi brez svojih odvisnosti.
  • Dodelite vbrizgano odvisnost polju / lastnosti samo za branje (da preprečite, da bi znotraj metode nehote dodelili drugo vrednost).

Vbrizgavanje lastnosti

Standardni vsebnik za vbrizgavanje odvisnosti ASP.NET Core ne podpira vbrizgavanja lastnosti. Lahko pa uporabite drugo posodo, ki podpira injekcijo lastnosti. Primer:

z uporabo Microsoft.Extensions.Logging;
z uporabo Microsoft.Extensions.Logging.Abstractions;
imenski prostor MyApp
{
    ProductService v javnem razredu
    {
        javni ILogger  Logger {get; komplet; }
        zasebno samo za branje IProductRepository _productRepository;
        javna storitev izdelka (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger  .Instance;
        }
        javna ničnost Izbriši (int id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Izbrisal izdelek z id = {id}");
        }
    }
}

ProductService razglaša lastnost Loggerja z javnim nastaviteljem. Zabojnik za vbrizgavanje odvisnosti lahko nastavi Logger, če je na voljo (registriran v posodi DI prej).

Dobre prakse:

  • Uporabljajte injekcijo lastnosti samo za neobvezne odvisnosti. To pomeni, da lahko vaša storitev brezhibno deluje brez teh odvisnosti.
  • Če je mogoče, uporabite Null Object Pattern (kot je v tem primeru). V nasprotnem primeru med uporabo odvisnosti vedno preverite ničnost.

Lokacijski servis

Vzorec lokatorja storitev je še en način pridobivanja odvisnosti. Primer:

ProductService v javnem razredu
{
    zasebno samo za branje IProductRepository _productRepository;
    zasebno samo za branje ILogger  _logger;
    javna storitev izdelka (storitev IServiceProviderProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = storitevProvider
          .GetService > () ??
            NullLogger  .Instance;
    }
    javna ničnost Izbriši (int id)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "Izbrisan izdelek z id = {id}");
    }
}

ProductService vbrizgava IServiceProvider in z njim odpravlja odvisnosti. GetRequiredService vrže izjemo, če zahtevana odvisnost prej ni bila registrirana. Po drugi strani GetService v tem primeru samo vrne ničelno.

Ko rešite storitve znotraj konstruktorja, se sprostijo, ko se storitev sprosti. Torej vam ni vseeno, da bi sprostili / odstranili storitve, ki jih rešite znotraj konstruktorja (tako kot konstruktor in vbrizgavanje lastnosti).

Dobre prakse:

  • Kadar koli je mogoče, ne uporabljajte vzorca lokatorja storitev (če je vrsta storitve znana v času razvoja). Ker naredi odvisnosti implicitne. To pomeni, da med ustvarjanjem primerka storitve ni mogoče preprosto videti odvisnosti. To je še posebej pomembno pri enotnih testih, kjer se boste morda želeli zasmehovati glede nekaterih odvisnosti storitve.
  • Če je mogoče, rešite odvisnosti v storitvenem konstruktorju. Če rešite način storitve, bo vaša aplikacija bolj zapletena in nagnjena k napakam. Težave in rešitve bom opisal v naslednjih razdelkih.

Service Life Times

V aplikaciji ASP.NET Core Dependency Injection so tri življenjske dobe:

  1. Prehodne storitve se ustvarijo vsakič, ko jih injicirate ali zahtevate.
  2. Obseg storitev je ustvarjen po obsegu. V spletni aplikaciji vsaka spletna zahteva ustvari nov ločen obseg storitev. To pomeni, da so obsežne storitve običajno ustvarjene na spletni zahtevi.
  3. Singleton storitve so ustvarjene na posodo DI. To na splošno pomeni, da jih ustvarite samo enkrat na aplikacijo in jih nato uporabite za celotno življenjsko dobo aplikacije.

DI zabojnik spremlja vse rešene storitve. Storitve se sprostijo in odstranijo po koncu življenjske dobe:

  • Če je storitev odvisna, se tudi samodejno sprostijo in odstranijo.
  • Če storitev uporablja vmesnik IDisposable, se ob izdaji storitve samodejno pokliče metoda Dispose.

Dobre prakse:

  • Kadar koli je to mogoče, registrirajte svoje storitve kot prehodne. Ker je preprosto oblikovati prehodne storitve. Na splošno vas ne zanimajo večkratni navoji in puščanje spomina in veste, da ima storitev kratko življenjsko dobo.
  • Pazljivo uporabljajte obseg življenjske dobe, saj je lahko težavno, če ustvarite obseg otroških storitev ali uporabljate te storitve iz ne-spletne aplikacije.
  • Pazljivo uporabljajte enojno življenjsko dobo, od takrat se morate spoprijeti z večkratnimi navoji in potencialnimi težavami s puščanjem spomina.
  • Ne odvisni od prehodne ali obsežne storitve od storitve Singleton. Ker prehodna storitev postane singleton primer, ko singleton storitev vbrizga, in to lahko povzroči težave, če prehodna storitev ni zasnovana za takšen scenarij. Privzeti zabojnik DI ASP.NET Core že v takih primerih vrže izjeme.

Reševanje storitev v metodičnem organu

V nekaterih primerih boste morda morali rešiti drugo storitev na način svoje storitve. V takšnih primerih poskrbite, da boste storitev sprostili po uporabi. Najboljši način za to je ustvariti obseg storitve. Primer:

javni razred PriceCalculator
{
    zasebno samo za branje IServiceProvider _serviceProvider;
    javni cenovni kalkulator (storitev IServiceProviderProvider)
    {
        _serviceProvider = storitevProvider;
    }
    javni plovec Izračunaj (izdelek izdelka, število int,
      Vnesite davekStrategyServiceType)
    {
        z uporabo (var obseg = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) obseg.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            var cena = izdelek.Cena * štetje;
            povratna cena + davekStrategy.CalculateTax (cena);
        }
    }
}

PriceCalculator v svoj konstruktor vbrizga IServiceProvider in ga dodeli polju. PriceCalculator jo nato uporabi znotraj metode izračuna, da ustvari obseg otroških storitev. Za reševanje storitev uporablja obsegServiceProvider namesto vbrizganega primerka _serviceProvider. Tako se vse storitve, ki so rešene iz področja, samodejno sprostijo / odstranijo na koncu uporabnega stavka.

Dobre prakse:

  • Če storitev rešujete v organu metode, vedno ustvarite obseg otroške storitve, da zagotovite, da so rešene storitve pravilno sproščene.
  • Če metoda dobi IServiceProvider kot argument, potem lahko storitve neposredno rešite iz nje, ne da bi jih bilo treba sprostiti / odstraniti. Za ustvarjanje / upravljanje obsega storitve je odgovorna koda, ki kliče vašo metodo. Če upoštevate to načelo, bo vaša koda čistejša.
  • Ne sklicujte se na rešeno storitev! V nasprotnem primeru lahko pride do puščanja pomnilnika in do poznejše uporabe referenčnega predmeta boste imeli dostop do odstranjene storitve (razen če je rešena storitev enotna).

Singleton storitve

Storitve Singleton so na splošno zasnovane tako, da ohranjajo stanje aplikacije. Predpomnilnik je dober primer stanja aplikacij. Primer:

FileService javnega razreda
{
    zasebno samo za branje ConcurrentDictionary  _cache;
    javna FileService ()
    {
        _cache = nov ConcurrentDictionary  ();
    }
    javni bajt [] GetFileContent (string filePath)
    {
        return _cache.GetOrAdd (filePath, _ =>
        {
            vrne File.ReadAllBytes (filePath);
        });
    }
}

FileService preprosto predpomni vsebino datoteke, da zmanjša branje diskov. Ta storitev mora biti registrirana kot samska. V nasprotnem primeru predpomnjenje ne bo delovalo po pričakovanjih.

Dobre prakse:

  • Če ima storitev državo, bi morala do tega stanja dostopati z navojem. Ker vse zahteve hkrati uporabljajo isti primerek storitve. Za zagotovitev varnosti niti sem uporabil ConcurrentDictionary namesto slovarja.
  • Ne uporabljajte obsežnih ali prehodnih storitev od singleton storitev. Ker prehodne storitve morda niso zasnovane tako, da bi bile varne z nitmi. Če jih morate uporabljati, med uporabo teh storitev bodite pozorni na več navojev (na primer uporabite ključavnico).
  • Popuščanje pomnilnika na splošno povzročajo storitve singleton. Do konca prijave se ne sprostijo / odstranijo. Če bodo vzpostavili razrede (ali jih vbrizgali), vendar jih ne bodo sprostili / odstranili, bodo ostali tudi v spominu do konca aplikacije. Poskrbite, da jih boste sprostili / odstranili ob pravem času. Oglejte si razreševanje storitev v razdelku Telo metode zgoraj.
  • Če predpomnite podatke (vsebina datoteke v tem primeru), morate ustvariti mehanizem za posodobitev / razveljavitev predpomnjenih podatkov, ko se izvorni vir podatkov spremeni (ko se za ta primer spremeni predpomnjena datoteka na disku).

Obseg storitev

Najprej se zdi, da je obseg življenjske dobe dober kandidat za shranjevanje podatkov na spletni zahtevi. Ker ASP.NET Core ustvari obseg storitve na spletno zahtevo. Če torej storitev registrirate v obsegu, jo lahko delite med spletno zahtevo. Primer:

javni razred RequestItemsService
{
    zasebni samo za branje slovar  _items;
    javni RequestItemsService ()
    {
        _items = nov slovar  ();
    }
    javna ničnost Set (ime niza, vrednost predmeta)
    {
        _items [ime] = vrednost;
    }
    javni objekt Get (ime niza)
    {
        return _items [ime];
    }
}

Če registrirate RequestItemsService kot obseg in ga vstavite v dve različni storitvi, potem lahko dobite element, ki je dodan iz druge storitve, ker bosta delila isti primer zahteve RequestItemsService. To pričakujemo od obsežnih storitev.

Ampak .. dejstvo morda ni vedno tako. Če ustvarite obseg otroške storitve in rešite RequestItemsService iz podrejenega področja, potem boste dobili nov primerek RequestItemsService in ta ne bo deloval, kot ste pričakovali. Torej, obsežna storitev ne pomeni vedno primerka na spletno zahtevo.

Morda mislite, da ne naredite tako očitne napake (razrešitev obsega znotraj otrokovega področja). Vendar to ni napaka (zelo običajna uporaba) in primer morda ni tako preprost. Če je med vašimi storitvami velik graf odvisnosti, ne morete vedeti, ali je kdo ustvaril otroški obseg in rešil storitev, ki vbrizga drugo storitev ... ki končno vbrizga obsežno storitev.

Dobra praksa:

  • Storitev z obsegom je mogoče razumeti kot optimizacijo, če jo v spletno zahtevo vbrizga preveč storitev. Tako bodo vse te storitve med isto spletno zahtevo uporabile en primerek storitve.
  • Obsežne storitve ni treba oblikovati kot varne z nitmi. Ker jih običajno uporablja ena sama spletna zahteva / nit. Toda ... v tem primeru ne bi smeli deliti obsegov storitev med različne niti!
  • Bodite previdni, če oblikujete storitev z obsegom za izmenjavo podatkov med drugimi storitvami v spletni zahtevi (razloženo zgoraj). Podatke o spletnih zahtevah lahko shranite znotraj HttpContext (za dostop do njega vstavite IHttpContextAccessor), kar je varnejši način. Življenjska doba HttpContext ni zajeta. Pravzaprav sploh ni registriran v DI (zato ga ne injicirate, ampak namesto njega vstavite IHttpContextAccessor). Izvedba HttpContextAccessor uporablja AsyncLocal za skupno rabo istega HttpContext med spletno zahtevo.

Zaključek

Vbrizg odvisnosti se na začetku zdi preprost, vendar obstajajo morebitne težave z več navoji in uhajanjem pomnilnika, če ne upoštevate nekaterih strogih načel. Delil sem nekaj dobrih načel, ki temeljijo na mojih izkušnjah med razvojem okvira ASP.NET Boilerplate.