ASP.NET pamata atkarības injekcijas paraugprakse, padomi un viltības

Šajā rakstā es dalīšos ar savu pieredzi un ieteikumiem par atkarības injekcijas izmantošanu ASP.NET Core lietojumprogrammās. Šo principu motivācija ir:

  • Efektīva pakalpojumu un to atkarību projektēšana.
  • Vairāku pavedienu novēršana.
  • Atmiņas noplūdes novēršana.
  • Potenciālo kļūdu novēršana.

Šajā rakstā tiek pieņemts, ka pamatlīmenī jau esat pazīstams ar atkarības ievadīšanu un ASP.NET Core. Ja nē, lūdzu, vispirms izlasiet ASP.NET Core Dependency Injection dokumentāciju.

Pamati

Konstruktora iesmidzināšana

Konstruktora iesmidzināšanu izmanto, lai deklarētu un iegūtu pakalpojuma atkarības no pakalpojuma konstrukcijas. Piemērs:

sabiedriskās klases produkts
{
    privāti lasāms tikai IProductRepository _productRepository;
    publiskais produktu pakalpojums (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    public void Dzēst (int id)
    {
        _productRepository.Delete (id);
    }
}

ProductService injicē IProductRepository kā atkarību no tā konstruktora, pēc tam izmantojot metodi Dzēst.

Labā prakse:

  • Pakalpojumu konstruktorā skaidri definējiet nepieciešamās atkarības. Tādējādi pakalpojumu nevar izveidot bez tā atkarībām.
  • Piešķiriet ievadīto atkarību tikai lasāmajam laukam / īpašumam (lai nejauši metodē nejauši nepiešķirtu citu vērtību).

Īpašuma iesmidzināšana

ASP.NET Core standarta atkarības injekcijas konteiners neatbalsta īpašuma injekciju. Bet jūs varat izmantot citu konteineru, kas atbalsta īpašuma injekciju. Piemērs:

izmantojot Microsoft.Extensions.Logging;
izmantojot Microsoft.Extensions.Logging.Abstraction;
nosaukumvieta MyApp
{
    sabiedriskās klases produkts
    {
        publiskais ILogger  Logger {get; komplekts; }
        privāti lasāms tikai IProductRepository _productRepository;
        publiskais produktu pakalpojums (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger  .Instance;
        }
        public void Dzēst (int id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Dzēsts produkts ar id = {id}");
        }
    }
}

ProductService deklarē Logger īpašumu ar publisku iestatītāju. Atkarības injekcijas tvertne var iestatīt reģistrētāju, ja tas ir pieejams (iepriekš reģistrēts DI konteinerā).

Labā prakse:

  • Īpašuma injekciju izmantojiet tikai pēc izvēles atkarībām. Tas nozīmē, ka jūsu pakalpojums var pareizi darboties bez šīm atkarībām.
  • Ja iespējams, izmantojiet Null Object Pattern (tāpat kā šajā piemērā). Pretējā gadījumā, izmantojot atkarību, vienmēr pārbaudiet, vai tajā nav nulles.

Pakalpojumu meklētājs

Pakalpojumu lokatora paraugs ir vēl viens veids, kā iegūt atkarības. Piemērs:

sabiedriskās klases produkts
{
    privāti lasāms tikai IProductRepository _productRepository;
    privāti lasāms ILogger  _logger;
    publisks produktu pakalpojums (IServiceProvider serviceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            NullLogger  .Instance;
    }
    public void Dzēst (int id)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "Dzēsts produkts ar id = {id}");
    }
}

ProductService injicē IServiceProvider un, izmantojot to, novērš atkarības. GetRequiredService ir izņēmums, ja pieprasītā atkarība iepriekš nav reģistrēta. No otras puses, GetService tādā gadījumā vienkārši atgriežas spēkā.

Atrisinot pakalpojumus konstruktorā, tie tiek atbrīvoti, kad pakalpojums tiek izlaists. Tātad jums nav jārūpējas par tādu pakalpojumu izlaišanu / atsavināšanu, kas ir atrisināti konstruktora iekšienē (tāpat kā konstruktora un īpašuma iepludināšana).

Labā prakse:

  • Cik vien iespējams, neizmantojiet pakalpojuma lokatora shēmu (ja pakalpojuma veids ir zināms izstrādes laikā). Jo tas padara atkarības netiešas. Tas nozīmē, ka, veidojot pakalpojuma gadījumu, nav viegli saskatīt atkarības. Tas ir īpaši svarīgi vienību pārbaudēs, kurās varat izsmiet dažas pakalpojuma atkarības.
  • Ja iespējams, atrisiniet atkarības pakalpojumu konstruktorā. Atrisināšana apkalpošanas metodē padara jūsu lietojumprogrammu sarežģītāku un rada kļūdas. Es apskatīšu problēmas un risinājumus nākamajās sadaļās.

Kalpošanas laiks

ASP.NET pamata atkarības injekcijā ir trīs kalpošanas laiki:

  1. Pārejoši pakalpojumi tiek izveidoti katru reizi, kad tos injicē vai pieprasa.
  2. Pakalpojumu joma tiek izveidota pa darbības jomām. Tīmekļa lietojumprogrammā katrs tīmekļa pieprasījums rada jaunu, nodalītu pakalpojumu jomu. Tas nozīmē, ka apjomīgi pakalpojumi parasti tiek izveidoti katram tīmekļa pieprasījumam.
  3. Singletona pakalpojumi tiek izveidoti katram DI konteineram. Tas parasti nozīmē, ka tie tiek izveidoti tikai vienu reizi vienā lietojumprogrammā un pēc tam tiek izmantoti visu lietojumprogrammas darbības laiku.

DI konteiners seko visiem atrisinātajiem pakalpojumiem. Pakalpojumus atbrīvo un atsavina, kad beidzas to kalpošanas laiks:

  • Ja pakalpojumam ir atkarības, tie arī tiek automātiski atbrīvoti un iznīcināti.
  • Ja pakalpojums ievieš IDisposable interfeisu, pakalpojuma izlaišanā automātiski tiek izmantota Dispose metode.

Labā prakse:

  • Reģistrējiet savus pakalpojumus kā īslaicīgus, kur vien iespējams. Tā kā īslaicīgu pakalpojumu izstrāde ir vienkārša. Jums parasti nerūp daudzkārtīga vītne un atmiņas noplūde, un jūs zināt, ka pakalpojumam ir īss mūžs.
  • Rūpīgi izmantojiet visaptveroša pakalpojuma kalpošanas laiku, jo tas var būt sarežģīti, ja izveidojat bērnu pakalpojumu tvērumus vai izmantojat šos pakalpojumus no tīmekļa lietojumprogrammas.
  • Kopš tā laika uzmanīgi izmantojiet singletona kalpošanas laiku, tāpēc jums ir jārisina vairāku pavedienu un iespējamās atmiņas noplūdes problēmas.
  • Neatkarieties no īslaicīga vai apjomīga pakalpojuma no atsevišķa pakalpojuma. Tā kā īslaicīgs pakalpojums kļūst par vienreizēju gadījumu, kad to iepludina atsevišķs pakalpojums, un tas var radīt problēmas, ja īslaicīgais pakalpojums nav paredzēts šāda scenārija atbalstam. ASP.NET Core noklusējuma DI konteiners šādos gadījumos jau rada izņēmumus.

Pakalpojumu risināšana metodes korpusā

Dažos gadījumos jums, iespējams, būs jāatrisina cits pakalpojums, izmantojot jūsu pakalpojuma metodi. Šādos gadījumos pārliecinieties, vai pakalpojums tiek atbrīvots pēc lietošanas. Labākais veids, kā to nodrošināt, ir pakalpojuma jomas izveidošana. Piemērs:

sabiedriskās klases PriceCalculator
{
    privāti lasāms IServiceProvider _serviceProvider;
    publisks cenu kalkulators (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    aprēķināt publisko pludiņu (produkta produkts, int skaits,
      Ierakstiet taxStrategyServiceType)
    {
        izmantojot (var ulatums = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) darbības joma.Pakalpojuma sniedzējs
              .GetRequiredService (taxStrategyServiceType);
            var cena = prece.Cena * skaits;
            atgriešanas cena + taxStrategy.CalculateTax (cena);
        }
    }
}

PriceCalculator ievada IServiceProvider savā konstruktorā un piešķir to laukam. Pēc tam PriceCalculator to izmanto aprēķināšanas metodes iekšienē, lai izveidotu bērnu pakalpojuma jomu. Pakalpojumu risināšanai tas izmanto activ.ServiceProvider, nevis ievadīto _serviceProvider instanci. Tādējādi visi pakalpojumi, kas nošķirti no darbības jomas, tiek automātiski atbrīvoti / atsavināti lietojuma paziņojuma beigās.

Labā prakse:

  • Ja jūs risināt pakalpojumu metodiskajā korpusā, vienmēr izveidojiet bērnu pakalpojumu jomu, lai nodrošinātu, ka atrisinātie pakalpojumi tiek pienācīgi atbrīvoti.
  • Ja kā metodi kā metodi izmanto IServiceProvider, tad no tās pakalpojumus varat tieši atrisināt, neuztraucoties par atbrīvošanu / iznīcināšanu. Pakalpojuma jomas izveidošana / pārvaldīšana ir atbildīga par kodu, kas izsauc jūsu metodi. Ievērojot šo principu, jūsu kods kļūst tīrāks.
  • Neturiet atsauci uz atrisinātu pakalpojumu! Pretējā gadījumā tas var izraisīt atmiņas noplūdi, un jūs piekļūsit atsauktajam pakalpojumam, kad vēlāk izmantosit objekta atsauci (ja vien atrisinātais pakalpojums nav atsevišķs).

Singletona pakalpojumi

Singletona pakalpojumi parasti ir izstrādāti, lai saglabātu lietojumprogrammas stāvokli. Kešatmiņa ir labs lietojumprogrammu stāvokļu piemērs. Piemērs:

sabiedriskās klases FileService
{
    privāti lasāma vienlaicīga vārdnīca  _cache;
    publiskais FileService ()
    {
        _cache = jauna vienlaicīga vārdnīca  ();
    }
    publisks baits [] GetFileContent (virknes filePath)
    {
        atgriezt _cache.GetOrAdd (filePath, _ =>
        {
            atgriezt File.ReadAllBytes (filePath);
        });
    }
}

FileService vienkārši saglabā kešatmiņā faila saturu, lai samazinātu diska lasāmību. Šis pakalpojums jāreģistrē kā singletons. Pretējā gadījumā kešatmiņas saglabāšana nedarbosies, kā paredzēts.

Labā prakse:

  • Ja pakalpojumam ir stāvoklis, tam vajadzētu piekļūt, izmantojot drošu pavedienu. Tā kā visi pieprasījumi vienlaikus izmanto to pašu pakalpojuma gadījumu. Lai nodrošinātu diegu drošību, vārdnīcas vietā izmantoju ConcurrentDictionary.
  • Nelietojiet ierobežotus vai īslaicīgus pakalpojumus no atsevišķiem pakalpojumiem. Tā kā īslaicīgie pakalpojumi, iespējams, nav izstrādāti tā, lai būtu droši pavedieni. Ja jums tie ir jāizmanto, tad, lietojot šos pakalpojumus, rūpējieties par vairāku pavedienu veidošanu (piemēram, izmantojiet slēdzeni).
  • Atmiņas noplūdi parasti izraisa singletona pakalpojumi. Tie netiek atbrīvoti / iznīcināti līdz pieteikuma beigām. Tātad, ja viņi instantizē klases (vai injicē), bet neatbrīvo / neizmet tās, viņi arī paliks atmiņā līdz lietojumprogrammas beigām. Pārliecinieties, ka jūs tos atbrīvojat / iznīcināt pareizajā laikā. Skatiet sadaļu “Atrisināšanas pakalpojumi metožu ķermenī”.
  • Ja kešatmiņā glabājat datus (faila saturs šajā piemērā), jums vajadzētu izveidot mehānismu kešatmiņā saglabāto datu atjaunināšanai / atzīšanai par nederīgiem, mainoties sākotnējam datu avotam (ja šajā piemērā mainās kešatmiņā saglabātais fails diskā).

Darbības joma

Pirmais darbības laiks, šķiet, ir labs kandidāts, lai glabātu datus par katru tīmekļa pieprasījumu. Tā kā ASP.NET Core katram tīmekļa pieprasījumam rada pakalpojuma jomu. Tātad, ja jūs reģistrējat pakalpojumu kā darbības jomu, to var koplietot tīmekļa pieprasījuma laikā. Piemērs:

publiskās klases RequestItemsService
{
    privāti lasāma vārdnīca  _items;
    public RequestItemsService ()
    {
        _items = jauna vārdnīca  ();
    }
    public void Set (virknes nosaukums, objekta vērtība)
    {
        _items [nosaukums] = vērtība;
    }
    publisks objekts Get (virknes nosaukums)
    {
        atgriezt _items [nosaukums];
    }
}

Ja reģistrējat RequestItemsService kā darbības jomu un iepludināt to divos dažādos pakalpojumos, jūs varat iegūt vienumu, kas pievienots no cita pakalpojuma, jo tiem būs kopīga pati RequestItemsService instance. To mēs sagaidām no visaptverošiem pakalpojumiem.

Bet .. fakts ne vienmēr ir tāds. Ja izveidosit bērnu pakalpojumu sfēru un no bērnu jomas novērsīsit RequestItemsService, tad jūs iegūsit jaunu RequestItemsService gadījumu, un tas nedarbosies, kā jūs gaidījāt. Tātad, apjomīgs pakalpojums ne vienmēr nozīmē gadījumu katrā tīmekļa pieprasījumā.

Jūs varat domāt, ka nepieņemat tik acīmredzamu kļūdu (atrisināt darbības jomu bērna darbības jomā). Tomēr tā nav kļūda (ļoti regulāra lietošana), un gadījums var nebūt tik vienkāršs. Ja starp jūsu pakalpojumiem ir liela atkarības diagramma, jūs nevarat zināt, vai kāds ir izveidojis darbības jomu bērniem un izvēlējies pakalpojumu, kas ievada citu pakalpojumu ... kas visbeidzot ievada ierobežotu pakalpojumu.

Laba prakse:

  • Apjoma pakalpojumu var uzskatīt par optimizāciju, ja to Web pieprasījumā ievada pārāk daudz pakalpojumu. Tādējādi visi šie pakalpojumi viena tīmekļa pieprasījuma laikā izmantos vienu pakalpojuma gadījumu.
  • Paplašinātie pakalpojumi nav jāveido kā droši pavedieni. Tāpēc, ka parasti tie būtu jāizmanto vienam tīmekļa pieprasījumam / pavedienam. Bet… tādā gadījumā jums nevajadzētu dalīties ar pakalpojumu sfērām starp dažādiem pavedieniem!
  • Esiet piesardzīgs, ja projektējat apjomīgu pakalpojumu, lai tīmekļa pieprasījumā koplietotu datus starp citiem pakalpojumiem (paskaidrots iepriekš). HttpContext var glabāt datus par katru tīmekļa pieprasījumu (lai tam piekļūtu IHttpContextAccessor), kas ir drošākais veids, kā to izdarīt. HttpContext darbības laiks netiek ierobežots. Faktiski tas vispār nav reģistrēts DI (tāpēc jūs to neveicat, bet injicējat IHttpContextAccessor). HttpContextAccessor ieviešana izmanto AsyncLocal, lai tīmekļa pieprasījuma laikā koplietotu to pašu HttpContext.

Secinājums

Sākumā šķiet, ka atkarības injekcija ir vienkārša, taču, ja neievērosit dažus stingrus principus, pastāv potenciālas vairāku pavedienu un atmiņas noplūdes problēmas. Es dalījos ar dažiem labiem principiem, balstoties uz manu pieredzi ASP.NET katlu paneļa ietvara izstrādes laikā.