„ASP.NET Core priklausomybės įpurškimo geriausia praktika, patarimai ir gudrybės“

Šiame straipsnyje pasidalinsiu savo patirtimi ir pasiūlymais dėl priklausomybės injekcijos naudojimo ASP.NET Core programose. Šiuos principus lemia:

  • Efektyvus paslaugų ir jų priklausomybių projektavimas.
  • Užkirsti kelią daugialypiams klausimams.
  • Atminties nutekėjimo prevencija.
  • Galimų klaidų prevencija.

Šiame straipsnyje daroma prielaida, kad jūs jau esate susipažinęs su Priklausomybės įpurškimu ir ASP.NET Core pagrindiniu lygiu. Jei ne, pirmiausia perskaitykite ASP.NET pagrindinės priklausomybės įpurškimo dokumentus.

Pagrindai

Konstruktoriaus įpurškimas

Konstruktoriaus įpurškimas naudojamas deklaruoti ir gauti paslaugos priklausomybes nuo konstrukcijos. Pavyzdys:

viešosios klasės „ProductService“
{
    privatus, tik skaitomas IProductRepository _productRepository;
    vieša „ProductService“ („IProductRepository productRepository“)
    {
        _productRepository = productRepository;
    }
    public void ištrinti (int id)
    {
        _productRepository.Delete (id);
    }
}

„ProductService“ injekuoja „IProductRepository“ kaip priklausomybę nuo jo konstruktoriaus, tada naudoja jį „Delete“ metodo viduje.

Geroji patirtis:

  • Aiškiai apibrėžkite reikiamas priklausomybes paslaugų konstruktoriuje. Taigi paslaugos negalima sukurti be jos priklausomybių.
  • Priskirkite įvestą priklausomybę tik skaitomam laukui / ypatybei (kad metodo viduje netyčia nepriskirtumėte kitos vertės).

Turto įpurškimas

„ASP.NET Core“ standartinis priklausomybės įpurškimo indas nepalaiko nuosavybės įpurškimo. Bet jūs galite naudoti kitą talpyklą, palaikančią turto injekciją. Pavyzdys:

naudojant „Microsoft.Extensions.Logging“;
naudojant „Microsoft.Extensions.Logging.Abstraction“;
vardų sritis „MyApp“
{
    viešosios klasės „ProductService“
    {
        public ILogger  Logger {get; rinkinys; }
        privatus, tik skaitomas IProductRepository _productRepository;
        vieša „ProductService“ („IProductRepository productRepository“)
        {
            _productRepository = productRepository;
            Logger = NullLogger  .Instance;
        }
        public void ištrinti (int id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Ištrynė produktą, kurio id = {id}");
        }
    }
}

„ProductService“ skelbia „Logger“ nuosavybę viešuoju rinkėju. Priklausomybės injekcijos konteineris gali nustatyti registratorių, jei jo yra (anksčiau užregistruotas DI konteineryje).

Geroji patirtis:

  • Turto injekciją naudokite tik pasirinktinėms priklausomybėms. Tai reiškia, kad jūsų tarnyba gali tinkamai veikti be šių priklausomybių.
  • Jei įmanoma, naudokite Null Object Pattern (kaip šiame pavyzdyje). Priešingu atveju, naudodamiesi priklausomybe, visada patikrinkite, ar nėra nulio.

Paslaugų ieškiklis

Aptarnavimo vietos nustatymo schema yra dar vienas būdas gauti priklausomybes. Pavyzdys:

viešosios klasės „ProductService“
{
    privatus, tik skaitomas IProductRepository _productRepository;
    asmeninis skaitymas ILogger  _logger;
    vieša „ProductService“ („IServiceProvider serviceProvider“)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            „NullLogger“  .Instance;
    }
    public void ištrinti (int id)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "Ištrynė produktą, kurio id = {id}");
    }
}

„ProductService“ suleidžia „IServiceProvider“ ir išsprendžia priklausomybes naudodamasi ja. „GetRequiredService“ yra išimtis, jei reikalaujama priklausomybė nebuvo anksčiau užregistruota. Kita vertus, tokiu atveju „GetService“ grąžina tik negaliojančią.

Kai išspręsite paslaugas konstruktoriaus viduje, jos bus paleistos, kai paslauga bus paleista. Taigi, jums nerūpi išleisti / išmesti paslaugos, išspręstos konstruktoriaus viduje (kaip ir statytojo bei turto injekcijos).

Geroji patirtis:

  • Kur tik įmanoma, nenaudokite aptarnavimo vietos nustatymo modelio (jei paslaugos tipas žinomas kūrimo metu). Nes tai daro priklausomybes netiesiogines. Tai reiškia, kad kuriant paslaugos egzempliorių neįmanoma lengvai pamatyti priklausomybių. Tai ypač svarbu atliekant vienetų testus, kai galbūt norėsite pasityčioti iš kai kurių paslaugų priklausomybių.
  • Jei įmanoma, išspręskite priklausomybes paslaugų konstruktoriuje. Sprendimas aptarnavimo metodu padaro jūsų programą sudėtingesnę ir labiau linkusią į klaidas. Kituose skyriuose aptarsiu problemas ir sprendimus.

Tarnavimo laikas

Yra trys ASP.NET priklausomybės nuo įpurškimo tarnavimo laikai:

  1. Pereinamosios paslaugos sukuriamos kiekvieną kartą, kai jos sušvirkščiamos ar paprašomos.
  2. Taikomos paslaugos yra kuriamos pagal apimtį. Žiniatinklio programoje kiekviena interneto užklausa sukuria naują atskirtą paslaugų apimtį. Tai reiškia, kad apimties paslaugos paprastai sukuriamos pagal kiekvieną interneto užklausą.
  3. „Singleton“ paslaugos sukuriamos kiekvienam DI konteineriui. Paprastai tai reiškia, kad jie sukuriami tik vieną kartą per programą ir tada naudojami visą programos laiką.

DI konteineris seka visas išspręstas paslaugas. Paslaugos išleidžiamos ir parduodamos pasibaigus jų gyvavimo laikui:

  • Jei tarnyba turi priklausomybių, jos taip pat automatiškai išleidžiamos ir šalinamos.
  • Jei paslauga įgyvendina atpažįstamą sąsają, išleidimo būdas automatiškai iškviečiamas.

Geroji patirtis:

  • Kur tik įmanoma, užregistruokite savo paslaugas kaip trumpalaikes. Nes nesunku suprojektuoti trumpalaikes paslaugas. Paprastai nerūpi daugialypis sriegis ir atminties nutekėjimas, ir žinote, kad paslauga trunka neilgai.
  • Atidžiai naudokite apimties paslaugų teikimo laiką, nes gali būti sudėtinga, jei kuriate vaikų paslaugų apimtis arba šias paslaugas naudojate iš ne žiniatinklio programos.
  • Atsargiai naudokitės pavieniu gyvenimo periodu, nes tada jums reikia spręsti daugybės sriegių ir galimas atminties nutekėjimo problemas.
  • Nepriklauso nuo trumpalaikės ar apimties paslaugos nuo pavienių paslaugų. Kadangi trumpalaikė paslauga tampa atskira instancija, kai ją suleidžia atskira tarnyba, ir tai gali sukelti problemų, jei trumpalaikė paslauga nėra sukurta tokiam scenarijui palaikyti. „ASP.NET Core“ numatytasis DI konteineris tokiais atvejais jau pateikia išimtis.

Paslaugų sprendimas metodo kūne

Kai kuriais atvejais gali tekti išspręsti kitą paslaugą pagal jūsų paslaugos metodą. Tokiais atvejais įsitikinkite, kad po naudojimo pasinaudojote paslauga. Geriausias būdas tai užtikrinti yra sukurti paslaugų apimtį. Pavyzdys:

viešosios klasės „PriceCalculator“
{
    tik perskaitytas IServiceProvider _serviceProvider;
    viešas kainų skaičiuoklė („IServiceProvider“ paslaugos teikėjas)
    {
        _serviceProvider = serviceProvider;
    }
    apskaičiuoti (produkto produktas, int skaičius,
      Įveskite taxStrategyServiceType)
    {
        naudojant (var ulatas = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) apimtis.Paslaugos teikėjas
              .GetRequiredService („taxStrategyServiceType“);
            var kaina = produktas.Kaina * skaičiuoti;
            grąžinimo kaina + taxStrategy.CalculateTax (kaina);
        }
    }
}

„PriceCalculator“ įšvirkščia „IServiceProvider“ į konstruktorių ir priskiria jį laukui. Tada „PriceCalculator“ naudoja jį „Skaičiuoti“ viduje, kad sukurtų paslaugų vaikams aprėptį. Vietoj įšvirkšto _serviceProvider egzemplioriaus paslaugoms išspręsti naudojamas „activ.ServiceProvider“. Taigi visos paslaugos, išspręstos iš apimties, yra automatiškai išleidžiamos / dislokuojamos naudojimo ataskaitos pabaigoje.

Geroji patirtis:

  • Jei sprendžiate paslaugą metodinėje įstaigoje, visada sukurkite paslaugų vaikams aprėptį, kad įsitikintumėte, jog išspręstos paslaugos yra tinkamai paleistos.
  • Jei metodas „IServiceProvider“ pateikiamas kaip argumentas, tuomet galite tiesiogiai išspręsti iš jo teikiamas paslaugas nesirūpindami, kad būtų paleista / pašalinta. Sukurti / valdyti paslaugų apimtį yra atsakingas už kodą, vadinantį jūsų metodu. Vadovaudamiesi šiuo principu, jūsų kodas taps švaresnis.
  • Neturėkite nuorodos į išspręstą paslaugą! Priešingu atveju tai gali sukelti atminties nutekėjimą ir jūs pateksite į pašalintą paslaugą, kai vėliau naudosite objekto nuorodą (nebent išspręsta paslauga yra atskira).

Singletono paslaugos

„Singleton“ paslaugos paprastai skirtos išlaikyti programos būseną. Talpykla yra geras programų būsenų pavyzdys. Pavyzdys:

viešosios klasės „FileService“
{
    privatus, tik skaitomas „ConcurrentDictionary“  _cache;
    vieša „FileService“ ()
    {
        _cache = nauja „ConcurrentD Dictionary“  ();
    }
    viešas baitas [] „GetFileContent“ (eilutės filePath)
    {
        grąžinti _cache.GetOrAdd („FilePath“, _ =>
        {
            grąžinti „File.ReadAllBytes“ (filePath);
        });
    }
}

„FileService“ tiesiog talpina failo turinį, kad sumažėtų disko skaitymas. Ši paslauga turėtų būti įregistruota kaip atskira įmonė. Priešingu atveju talpyklos kaupimas neveiks taip, kaip tikėtasi.

Geroji patirtis:

  • Jei paslauga turi būseną, ji turėtų prieiti prie tos būsenos saugiu gija. Kadangi visos užklausos tuo pačiu metu naudoja tą patį paslaugos egzempliorių. Norėdami užtikrinti siūlų saugą, vietoj žodyno naudojau „ConcurrentDictionary“.
  • Nenaudokite apimtų ar trumpalaikių paslaugų iš pavienių paslaugų. Todėl, kad trumpalaikės paslaugos negali būti suprojektuotos taip, kad būtų saugios siūlams. Jei turite jas naudoti, naudodamiesi šiomis paslaugomis, pasirūpinkite kelių sriegių naudojimu (pavyzdžiui, naudokite užraktą).
  • Atminties nutekėjimą paprastai sukelia vienetinės paslaugos. Jie nebus paleisti / pašalinti iki paraiškos pabaigos. Taigi, jei jie akimirksniu išmoks mokymus (arba suleis), bet neatleis / neišmes, jie taip pat liks atmintyje iki programos pabaigos. Įsitikinkite, kad juos paleisite / išmesite tinkamu laiku. Žr. Aukščiau esantį skyrių „Sprendimo paslaugos“ metodo kūne.
  • Jei talpykloje talpinate duomenis (failo turinys šiame pavyzdyje), turėtumėte sukurti mechanizmą, kuris atnaujins / padarys negaliojančius talpykloje esančius duomenis, kai pasikeis pirminis duomenų šaltinis (kai šio pavyzdžio metu talpykloje esantis failas keičiasi diske).

Taikomos paslaugos

Pirmiausia, atrodo, kad tinkama naudoti visą interneto užklausos informaciją. Kadangi ASP.NET Core sukuria paslaugos apimtį kiekvienai žiniatinklio užklausai. Taigi, jei užregistruojate paslaugą kaip neapimtą, ji gali būti dalijama žiniatinklio užklausos metu. Pavyzdys:

viešosios klasės „RequestItemsService“
{
    privatus skaitymo žodynas  _itemos;
    public RequestItemsService ()
    {
        _items = naujas žodynas  ();
    }
    „public void“ (eilutės pavadinimas, objekto vertė)
    {
        _items [vardas] = reikšmė;
    }
    viešas objektas Gauti (eilutės pavadinimas)
    {
        grąžinti _itemus [vardas];
    }
}

Jei užregistruosite „RequestItemsService“ kaip taikymo sritį ir įvesite ją į dvi skirtingas paslaugas, galėsite gauti elementą, pridėtą iš kitos paslaugos, nes jie turės tą pačią „RequestItemsService“ instanciją. Štai ko mes tikimės iš aprėpties paslaugų.

Bet .. faktas ne visada gali būti toks. Jei sukursite vaikų paslaugų apimtį ir išspręsite „RequestItemsService“ iš apimties vaikams, gausite naują „RequestItemsService“ egzempliorių ir ji neveiks taip, kaip tikėjotės. Taigi, apimties paslauga ne visada reiškia egzempliorių vienai interneto užklausai.

Galite pamanyti, kad nepadarėte tokios akivaizdžios klaidos (išspręsite aprėptį vaiko aprėptyje). Tačiau tai nėra klaida (labai įprastas vartojimas) ir atvejis gali būti ne toks jau paprastas. Jei tarp jūsų paslaugų yra didelis priklausomybės grafikas, jūs negalite žinoti, ar kas nors sukūrė vaiko apimtį ir išsprendė paslaugą, suteikiančią kitą paslaugą ... kuri pagaliau įveda apimties paslaugą.

Geroji praktika:

  • Apimties paslauga gali būti laikoma optimizavimu, kai į interneto užklausą įtraukia per daug paslaugų. Taigi visoms šioms paslaugoms bus naudojamas vienas paslaugos egzempliorius tos pačios žiniatinklio užklausos metu.
  • Taikomos paslaugos nebūtinai turi būti suplanuotos kaip saugios siūlams. Nes juos paprastai turėtų naudoti viena žiniatinklio užklausa / gija. Bet ... tokiu atveju neturėtumėte dalintis paslaugų sritimis tarp skirtingų gijų!
  • Būkite atsargūs, jei suprojektuosite apimties paslaugą, kad žiniatinklio užklausoje būtų galima dalytis duomenimis tarp kitų paslaugų (paaiškinta aukščiau). Kiekvienoje interneto užklausoje galite saugoti duomenis „HttpContext“ (šifruokite „IHttpContextAccessor“, kad galėtumėte prie jo prieiti) - tai yra saugesnis būdas tai padaryti. „HttpContext“ gyvavimo laikas netaikomas. Tiesą sakant, jis iš viso nėra registruotas DI (todėl ne švirkškite, bet švirkškite „IHttpContextAccessor“). „HttpContextAccessor“ diegimas naudoja „AsyncLocal“, kad žiniatinklio užklausos metu būtų dalijamasi tuo pačiu „HttpContext“.

Išvada

Iš pradžių atrodo, kad priklausomybės injekciją paprasta naudoti, tačiau jei nesilaikysite griežtų principų, gali kilti problemų dėl daugybės sriegių ir atminties nutekėjimo. Pasidaliniau keletu gerų principų, pagrįstų savo patirtimi kuriant ASP.NET katilinės sistemą.