ASP.NET-ydinriippuvuuspistoksen parhaat käytännöt, vinkit ja temput

Tässä artikkelissa jaan kokemukseni ja ehdotukseni riippuvuusinjektion käytöstä ASP.NET Core -sovelluksissa. Näiden periaatteiden taustalla on motivaatio;

  • Suunnittelemme tehokkaasti palvelut ja niiden riippuvuudet.
  • Monisäikeisten ongelmien estäminen.
  • Muistivuotojen estäminen.
  • Mahdollisten virheiden estäminen.

Tässä artikkelissa oletetaan, että tunnet jo riippuvuussuihkutuksen ja ASP.NET-ytimen perustasolla. Jos ei, lue ensin ASP.NET-ydinriippuvuusinjektio-ohjeet.

Perusasiat

Rakentajan injektio

Konstruktorin injektiota käytetään palvelun riippuvuuksien ilmoittamiseen ja saamiseen palvelurakenteesta. Esimerkki:

julkisen luokan ProductService
{
    yksityinen luku vain IProductRepository _productRepository;
    julkinen ProductService (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    public void Poista (int id)
    {
        _productRepository.Delete (id);
    }
}

ProductService injektoi IProductRepository riippuvuutena rakentamisessaan ja käyttää sitä Poista-menetelmän sisällä.

Hyvät käytännöt:

  • Määritä tarvittavat riippuvuudet nimenomaisesti palvelurakentajassa. Siksi palvelua ei voida rakentaa ilman sen riippuvuuksia.
  • Määritä injektoitu riippuvuus vain lukukelpoiselle kentälle / ominaisuudelle (estääksesi toisen arvon vahingossa antamisen sille menetelmän sisällä).

Kiinteistöjen injektio

ASP.NET Coren vakiovarainen injektiosäiliö ei tue omaisuuden injektiota. Mutta voit käyttää toista säiliötä, joka tukee omaisuuden injektiota. Esimerkki:

Microsoft.Extensions.Logging;
käyttämällä Microsoft.Extensions.Logging.Abstractions;
nimitila MyApp
{
    julkisen luokan ProductService
    {
        julkinen ILogger  Logger {get; sarja; }
        yksityinen luku vain IProductRepository _productRepository;
        julkinen ProductService (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger  .Instance;
        }
        public void Poista (int id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Poistettu tuote id = {id}");
        }
    }
}

ProductService julistaa Logger-ominaisuuden julkisella setterillä. Riippuvuusinsisäiliö voi asettaa hakkurin, jos se on käytettävissä (rekisteröity DI-säiliöön aiemmin).

Hyvät käytännöt:

  • Käytä kiinteistöjen injektiota vain valinnaisiin riippuvuuksiin. Tämä tarkoittaa sitä, että palvelusi voi toimia kunnolla ilman näitä riippuvuuksia.
  • Käytä nollaobjektikuviota (kuten tässä esimerkissä), jos mahdollista. Muussa tapauksessa tarkista aina nolla, kun käytät riippuvuutta.

Palvelun hakemisto

Palvelun paikannusmalli on toinen tapa saada riippuvuuksia. Esimerkki:

julkisen luokan ProductService
{
    yksityinen luku vain IProductRepository _productRepository;
    yksityinen luku vain ILogger  _logger;
    julkinen ProductService (IServiceProvider serviceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            NullLogger  .Instance;
    }
    public void Poista (int id)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "Poistettu tuote id = {id}");
    }
}

ProductService injektoi IServiceProviderin ja ratkaisee riippuvuudet sitä käyttämällä. GetRequiredService vie poikkeuksen, jos pyydettyä riippuvuutta ei ole rekisteröity aiemmin. Toisaalta GetService palauttaa vain nollan siinä tapauksessa.

Kun ratkaiset palvelut rakentajan sisällä, ne vapautetaan, kun palvelu vapautetaan. Joten et välitä rakentajan sisällä ratkaistujen palvelujen vapauttamisesta / hävittämisestä (aivan kuten rakentajan ja omaisuuden lisääminen).

Hyvät käytännöt:

  • Älä käytä palvelun paikannusmallia aina kun mahdollista (jos palvelutyyppi tunnetaan kehitysvaiheessa). Koska se tekee riippuvuudet implisiittisiksi. Tämä tarkoittaa, että riippuvuuksia ei ole helppo nähdä palvelun esiintymää luotaessa. Tämä on erityisen tärkeää yksikkötesteissä, joissa saatat haluta pilkata joitain palvelun riippuvuuksia.
  • Ratkaise riippuvuudet palvelurakentajassa, jos mahdollista. Ratkaisu palvelumenetelmällä tekee sovelluksesta monimutkaisemman ja virheille alttiimman. Tarkastelen seuraavien osien ongelmia ja ratkaisuja.

Käyttöiän ajat

ASP.NET-ydinriippuvuusinjektiossa on kolme käyttöaikaa:

  1. Tilapäiset palvelut luodaan aina, kun niitä injektoidaan tai pyydetään.
  2. Laajennetut palvelut luodaan laajuutta kohti. Verkkosovelluksessa jokainen verkkopyyntö luo uuden erillisen palvelun laajuuden. Tämä tarkoittaa, että kattavat palvelut luodaan yleensä web-pyyntöä kohden.
  3. Singleton-palvelut luodaan DI-konttia kohti. Tämä tarkoittaa yleensä, että ne luodaan vain kerran sovellusta kohti ja käytetään sitten koko sovelluksen elinajan.

DI-kontti seuraa kaikkia ratkaistuja palveluita. Palvelut vapautetaan ja hävitetään, kun niiden elinaika päättyy:

  • Jos palvelulla on riippuvuuksia, ne myös vapautetaan ja hävitetään automaattisesti.
  • Jos palvelu toteuttaa tunnistettavan käyttöliittymän, hävitysmenetelmää kutsutaan automaattisesti palvelun julkaisussa.

Hyvät käytännöt:

  • Rekisteröi palvelut väliaikaisesti aina kun mahdollista. Koska ohimenevien palvelujen suunnittelu on helppoa. Et yleensä välitä monisäikeisestä ja muistivuodoista ja tiedät, että palvelulla on lyhyt käyttöikä.
  • Käytä laaja-alaista palvelun käyttöikää huolellisesti, koska se voi olla hankalaa, jos luot lapsipalvelualueita tai käytät näitä palveluita muusta kuin web-sovelluksesta.
  • Käytä yksinkerran käyttöikää huolellisesti, sillä jälkeen sinun on käsiteltävä monisäikeiset ja mahdolliset muistivuoto-ongelmat.
  • Älä ole riippuvainen yksittäispalvelun lyhytaikaisesta tai laaja-alaisesta palvelusta. Koska väliaikaisesta palvelusta tulee erillinen esimerkki, kun yksittäinen palvelu pistää sen, ja se voi aiheuttaa ongelmia, jos lyhytaikaista palvelua ei ole suunniteltu tukemaan tällaista skenaariota. ASP.NET Coren oletusarvoinen DI-säilö heittää jo poikkeuksia tällaisissa tapauksissa.

Palvelujen ratkaiseminen menetelmäkehossa

Joissain tapauksissa joudut ehkä ratkaisemaan toisen palvelun palvelumenetelmälläsi. Varmista tällaisissa tapauksissa, että vapautat palvelun käytön jälkeen. Paras tapa varmistaa, että luodaan palvelun laajuus. Esimerkki:

julkisen luokan hintalaskuri
{
    yksityinen luku vain IServiceProvider _serviceProvider;
    julkinen hintalaskuri (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    public float Laske (Tuotetuote, int.
      Kirjoita taxStrategyServiceType)
    {
        käyttäminen (var ulatus = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) laajuus.Palveluntarjoaja
              .GetRequiredService (taxStrategyServiceType);
            var hinta = tuote.Hinta * laske;
            paluuhinta + taxStrategy.CalculateTax (hinta);
        }
    }
}

PriceCalculator ruiskuttaa IServiceProviderin rakentajaan ja määrittää sen kenttään. PriceCalculator käyttää sitä sitten Laske-menetelmän sisällä lasten palvelualueen luomiseen. Se käyttää laajuutta.PalvelinProvider ratkaisemaan palvelut palvelun ruiskutetun _serviceProvider-ilmentymän sijasta. Siten kaikki palvelut, jotka on ratkaistu laajuudesta, vapautetaan / hävitetään automaattisesti käyttölausunnon lopussa.

Hyvät käytännöt:

  • Jos ratkaiset palvelua menetelmäkehyksessä, luo aina lapsipalvelun laajuus varmistaaksesi, että ratkaisetut palvelut vapautetaan oikein.
  • Jos menetelmä saa IServiceProviderin argumentiksi, voit ratkaista palvelut siitä suoraan huolehtimatta julkaisemisesta / hävittämisestä. Palvelun laajuuden luominen / hallinta on menetelmäsi kutsuvan koodin vastuulla. Tämän periaatteen noudattaminen tekee koodista puhtaamman.
  • Älä pidä viittausta ratkaistuun palveluun! Muutoin se voi aiheuttaa muistivuotoja ja käytät poistettua palvelua, kun käytät objektiviittausta myöhemmin (ellei ratkaistu palvelu ole erillinen).

Singleton-palvelut

Singleton-palvelut on yleensä suunniteltu pitämään sovellustila. Välimuisti on hyvä esimerkki sovellustiloista. Esimerkki:

julkisen luokan FileService
{
    yksityinen luku vain ConcurrentDictionary  _ välimuisti;
    julkinen FileService ()
    {
        _cache = uusi samanaikainen sanakirja  ();
    }
    julkinen tavu [] GetFileContent (merkkijono filePath)
    {
        palauta _cache.GetOrAdd (filePath, _ =>
        {
            palauta File.ReadAllBytes (filePath);
        });
    }
}

FileService välimuistiin tallentaa tiedoston sisällön levyn lukemisen vähentämiseksi. Tämä palvelu tulisi rekisteröidä yksinkerrana. Muutoin välimuisti ei toimi odotetulla tavalla.

Hyvät käytännöt:

  • Jos palvelulla on tila, sen pitäisi päästä siihen tilaan lankavarmalla tavalla. Koska kaikki pyynnöt käyttävät samanaikaisesti samaa palvelun ilmentymää. Käytin ConcurrentDictionary-sanakirjaa sanakirjan sijasta langan turvallisuuden varmistamiseksi.
  • Älä käytä yksipuolisten palveluiden kattamia tai tilapäisiä palveluita. Koska ohimeneviä palveluita ei välttämättä ole suunniteltu ketjuvarmoiksi. Jos joudut käyttämään niitä, huolehdi sitten monisäikeisestä käytöstä näitä palveluita käytettäessä (käytä esimerkiksi lukkoa).
  • Muistivuodot johtuvat yleensä singleton-palveluista. Niitä ei vapauteta / hävitetä sovelluksen loppuun mennessä. Joten jos he välittävät luokkia (tai pistävät), mutta eivät vapauta / hävitä niitä, he pysyvät myös muistissa sovelluksen loppuun asti. Varmista, että vapautat / hävitä ne oikeaan aikaan. Katso yllä oleva menetelmärungon osien Ratkaisupalvelut.
  • Jos tallennat välimuistiin tietoja (tiedostosisältö tässä esimerkissä), sinun tulisi luoda mekanismi päivittämään / mitätöimään välimuistissa olevat tiedot, kun alkuperäinen tietolähde muuttuu (kun välimuistitiedosto muuttuu levyllä tässä esimerkissä).

Soveltamisalaan kuuluvat palvelut

Ensimmäinen ulottuvuus näyttää hyvältä ehdokkaalta tallentaa verkkopyyntöjä koskevia tietoja. Koska ASP.NET Core luo palvelun laajuuden web-pyyntöä kohden. Joten jos rekisteröit palvelun laajuudeksi, sitä voidaan jakaa verkkopyynnön aikana. Esimerkki:

julkinen luokka RequestItemsService
{
    yksityinen luku vain sanakirja  _items;
    public RequestItemsService ()
    {
        _items = uusi sanakirja  ();
    }
    public void Set (merkkijonon nimi, objektiarvo)
    {
        _items [nimi] = arvo;
    }
    julkinen esine Hanki (merkkijonon nimi)
    {
        palauta _items [nimi];
    }
}

Jos rekisteröit RequestItemsService-sovelluksen laajuudeksi ja lisäät sen kahteen eri palveluun, voit saada alkion, joka on lisätty toisesta palvelusta, koska he jakavat saman RequestItemsService-esiintymän. Sitä me odotamme laaja-alaisista palveluista.

Mutta .. tosiasia ei välttämättä ole aina sellainen. Jos luot lapsipalvelualueen ja ratkaisee RequestItemsService ala-ala-alueelta, saat uuden RequestItemsService-esiintymän, eikä se toimi odotetulla tavalla. Joten, laajuinen palvelu ei tarkoita aina esiintymää web-pyyntöä kohden.

Saatat ajatella, että et tee niin ilmeistä virhettä (ratkaisee lapsille tarkoitetun laajuuden). Mutta tämä ei ole virhe (erittäin säännöllinen käyttö), eikä tapaus voi olla niin yksinkertainen. Jos palveluidesi välillä on suuri riippuvuuskaavio, et voi tietää, onko joku luonut lapsille tarkoitetun laajuuden ja ratkaissut palvelun, joka lisää toisen palvelun ... joka lopulta lisää palvelun laajuuden.

Hyvä käytäntö:

  • Laajuuspalvelua voidaan ajatella optimointina, jos liian monet palvelut lisäävät sen verkkopyyntöön. Siksi kaikki nämä palvelut käyttävät yhtä palvelun esiintymää saman verkkopyynnön aikana.
  • Laajennettuja palveluita ei tarvitse suunnitella langatonksi. Koska niitä pitäisi yleensä käyttää yksi web-pyyntö / säie. Mutta… tällöin sinun ei pitäisi jakaa palvelualueita eri ketjujen välillä!
  • Ole varovainen, kun suunnittelet laaja-alaista palvelua jakamaan tietoja muiden palveluiden välillä verkkopyynnössä (selitetty yllä). Voit tallentaa web-pyyntöjä koskevia tietoja HttpContext-sovellukseen (injektoida IHttpContextAccessor käyttämään sitä), mikä on turvallisin tapa tehdä se. HttpContextin elinaikaa ei ole määritelty. Itse asiassa sitä ei ole rekisteröity DI: hen ollenkaan (siksi et pistä sitä, vaan pistät sen sijaan IHttpContextAccessor). HttpContextAccessor-toteutus käyttää AsyncLocalia jakamaan sama HttpContext Web-pyynnön aikana.

johtopäätös

Riippuvuusinjektio näyttää aluksi yksinkertaiselta käytöltä, mutta mahdollisia monisäikeistämistä ja muistivuoto-ongelmia voi olla, jos et noudata joitain tiukkoja periaatteita. Jaoin joitain hyviä periaatteita, jotka perustuvat omiin kokemuksiini ASP.NET Boilerplate -kehyksen kehittämisen aikana.