ASP.NET Core Dependency Injection Best Practices, Tips & Tricks

I denne artikkelen vil jeg dele mine erfaringer og forslag om bruk av avhengighetsinjeksjon i ASP.NET Core-applikasjoner. Motivasjonen bak disse prinsippene er;

  • Utforming av tjenester og deres avhengigheter effektivt.
  • Forhindrer problemer med flere tråder.
  • Forhindrer minne-lekkasjer.
  • Forebygge potensielle feil.

Denne artikkelen forutsetter at du allerede er kjent med Dependency Injection og ASP.NET Core på et grunnleggende nivå. Hvis ikke, kan du lese ASP.NET Core Dependency Injection-dokumentasjonen først.

Grunnleggende

Konstruktørinjeksjon

Konstruktørinjeksjon brukes til å erklære og oppnå avhengigheter av en tjeneste på servicekonstruksjonen. Eksempel:

offentlig klasse ProductService
{
    private readonly IProductRepository _productRepository;
    public ProductService (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    public void Delete (int id)
    {
        _productRepository.Delete (id);
    }
}

ProductService injiserer IProductRepository som en avhengighet i konstruktøren og bruker den i Delete-metoden.

God praksis:

  • Definer nødvendige avhengigheter eksplisitt i servicekonstruktøren. Dermed kan ikke tjenesten konstrueres uten dens avhengigheter.
  • Tilordne injisert avhengighet til et skrivebeskyttet felt / egenskap (for å forhindre tilfeldig tildeling av en annen verdi til den i en metode).

Innsprøytning av eiendom

ASP.NET Core standard injeksjonsbeholder for avhengighet støtter ikke eiendomsinjeksjon. Men du kan bruke en annen beholder som støtter eiendomsinjeksjonen. Eksempel:

bruker Microsoft.Extensions.Logging;
bruker Microsoft.Extensions.Logging.Abstraksjoner;
navneområde MyApp
{
    offentlig klasse ProductService
    {
        public ILogger  Logger {get; sett; }
        private readonly IProductRepository _productRepository;
        public ProductService (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger . Stemning;
        }
        public void Delete (int id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Slettet et produkt med id = {id}");
        }
    }
}

ProductService deklarerer en Logger-eiendom med offentlig setter. Avhengighetsinjeksjonsbeholder kan stille inn loggeren hvis den er tilgjengelig (registrert i DI-beholder før).

God praksis:

  • Bruk eiendomsinjeksjon bare for valgfrie avhengigheter. Det betyr at tjenesten din kan fungere ordentlig uten disse leveringsavhengighetene.
  • Bruk null objektmønster (som i dette eksemplet) hvis mulig. Ellers, sjekk alltid for null mens du bruker avhengigheten.

Service Locator

Service locator-mønster er en annen måte å få avhengigheter på. Eksempel:

offentlig klasse ProductService
{
    private readonly IProductRepository _productRepository;
    privat readonly ILogger  _logger;
    public ProductService (IServiceProvider serviceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            NullLogger  .Instance;
    }
    public void Delete (int id)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "Slettet et produkt med id = {id}");
    }
}

ProductService injiserer IServiceProvider og løser avhengigheter ved å bruke den. GetRequiredService kaster unntak hvis den forespurte avhengigheten ikke var registrert før. På den annen side returnerer GetService bare null i så fall.

Når du løser tjenester i konstruktøren, frigjøres de når tjenesten blir utgitt. Så du bryr deg ikke om å slippe / avhende tjenester som er løst inne i konstruktøren (akkurat som konstruktør og eiendomsinjeksjon).

God praksis:

  • Ikke bruk servicetekstermønsteret der det er mulig (hvis servicetypen er kjent i utviklingstiden). Fordi det gjør avhengighetene implisitte. Det betyr at det ikke er mulig å se avhengighetene enkelt mens du oppretter en forekomst av tjenesten. Dette er spesielt viktig for enhetstester der du kanskje vil spotte noen avhengigheter av en tjeneste.
  • Løs avhengigheter i servicekonstruktøren hvis mulig. Å løse en servicemetode gjør søknaden din mer komplisert og feilutsatt. Jeg vil dekke problemene og løsningene i de neste seksjonene.

Service Life Times

Det er tre levetid i ASP.NET Core Dependency Injection:

  1. Forbigående tjenester opprettes hver gang de injiseres eller etterspørres.
  2. Skopetjenester opprettes per omfang. I en nettapplikasjon oppretter hver nettforespørsel et nytt adskilt tjenestefelt. Det betyr at scoped-tjenester vanligvis opprettes per nettforespørsel.
  3. Singleton-tjenester opprettes per DI-container. Det betyr generelt at de bare opprettes en gang per applikasjon og deretter brukes hele levetiden for applikasjonen.

DI container holder oversikt over alle løste tjenester. Tjenestene frigjøres og avhendes når deres levetid er slutt:

  • Hvis tjenesten har avhengigheter, frigjøres og avhendes de også automatisk.
  • Hvis tjenesten implementerer det IDisponerbare grensesnittet, blir disposisjonsmetoden automatisk kalt til tjenesteutgivelse.

God praksis:

  • Registrer tjenestene dine som forbigående der det er mulig. Fordi det er enkelt å designe forbigående tjenester. Du bryr deg vanligvis ikke om flere tråder og minne-lekkasjer, og at du vet at tjenesten har kort levetid.
  • Bruk levetid for scoped service nøye, siden det kan være vanskelig hvis du oppretter omfangstjenester for barn eller bruker disse tjenestene fra et ikke-nettprogram.
  • Bruk singleton levetid nøye siden du trenger å takle flertrådproblemer og potensielle hukommelseslekkasjeproblemer.
  • Ikke stole på en forbigående eller scoped-tjeneste fra en singleton-tjeneste. Fordi den forbigående tjenesten blir en singleton-forekomst når en singleton-tjeneste injiserer den, og det kan forårsake problemer hvis den forbigående tjenesten ikke er designet for å støtte et slikt scenario. ASP.NET Core standard DI-container kaster allerede unntak i slike tilfeller.

Å løse tjenester i en metodekropp

I noen tilfeller kan det hende du må løse en annen tjeneste i en metode for tjenesten din. I slike tilfeller må du forsikre deg om at du slipper tjenesten etter bruk. Den beste måten å sikre det er å skape et tjenesteomfang. Eksempel:

offentlig klasse PriceCalculator
{
    privat readonly IServiceProvider _serviceProvider;
    public PriceCalculator (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    public float Beregn (produktprodukt, antall teller,
      Skriv inn skattStrategyServiceType)
    {
        bruker (var scope = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) scope.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            var pris = produkt. Pris * teller;
            returpris + skattStrategi. Beregn pris (pris);
        }
    }
}

PriceCalculator injiserer IServiceProvider i konstruktøren og tildeler den til et felt. PriceCalculator bruker den deretter i beregningsmetoden for å skape et barneserviceareal. Den bruker scope.ServiceProvider for å løse tjenester, i stedet for den injiserte _serviceProvider-forekomsten. Dermed frigjøres / avhendes alle tjenester som blir løst fra omfanget på slutten av brukerklæringen.

God praksis:

  • Hvis du løser en tjeneste i en metodekropp, må du alltid opprette et barneservicefelt for å sikre at de løste tjenestene blir frigitt på riktig måte.
  • Hvis en metode får IServiceProvider som et argument, kan du direkte løse tjenester fra den uten å bry deg om å slippe / avhende. Å opprette / administrere tjenesteomfang er et ansvar for koden som kaller metoden din. Ved å følge dette prinsippet blir koden renere.
  • Ikke referer til en løst tjeneste! Ellers kan det føre til minnelekkasjer, og du får tilgang til en disponert tjeneste når du bruker objektreferansen senere (med mindre den løste tjenesten er singleton).

Singleton Services

Singleton-tjenester er generelt designet for å holde en applikasjonstilstand. En cache er et godt eksempel på applikasjonstilstander. Eksempel:

offentlig klasse FileService
{
    privat readonly ConcurrentDictionary  _cache;
    offentlig FileService ()
    {
        _cache = ny ConcurrentDictionary  ();
    }
    offentlig byte [] GetFileContent (streng filePath)
    {
        returner _cache.GetOrAdd (filePath, _ =>
        {
            returner File.ReadAllBytes (filePath);
        });
    }
}

FileService hurtigbufrer bare filinnholdet for å redusere lesing av disken. Denne tjenesten skal registreres som singleton. Ellers vil ikke hurtigbufring fungere som forventet.

God praksis:

  • Hvis tjenesten har en tilstand, skal den få tilgang til denne tilstanden på en tråd-sikker måte. Fordi alle forespørsler samtidig bruker den samme forekomsten av tjenesten. Jeg brukte ConcurrentDictionary i stedet for Dictionary for å sikre tråden sikkerhet.
  • Ikke bruk scoped- eller forbigående tjenester fra singleton-tjenester. Fordi forbigående tjenester kanskje ikke er designet for å være trådsikker. Hvis du må bruke dem, må du ta vare på flertråd mens du bruker disse tjenestene (bruk lås for eksempel).
  • Minnelekkasjer er vanligvis forårsaket av singleton-tjenester. De blir ikke løslatt / avhendt før søknadens slutt. Så hvis de instantiserer klasser (eller injiserer), men ikke slipper / avhender dem, vil de også forbli i minnet til slutten av applikasjonen. Forsikre deg om at du slipper / avhender dem til rett tid. Se Løsningstjenestene i en metodekroppsdel ​​ovenfor.
  • Hvis du bufrer data (filinnhold i dette eksemplet), bør du opprette en mekanisme for å oppdatere / ugyldiggjøre cache-dataene når den opprinnelige datakilden endres (når en hurtigbufret fil endres på disken for dette eksemplet).

Scoped Services

Scoped levetid virker først som en god kandidat til å lagre per data på nettforespørsel. Fordi ASP.NET Core skaper et tjenesteomfang per nettforespørsel. Så hvis du registrerer en tjeneste som scoped, kan den deles under en nettforespørsel. Eksempel:

offentlig klasse RequestItemsService
{
    privat readonly Dictionary  _items;
    public RequestItemsService ()
    {
        _items = new Dictionary  ();
    }
    public void Set (strengnavn, objektverdi)
    {
        _items [name] = verdi;
    }
    offentlig objekt Få (strengnavn)
    {
        return _items [name];
    }
}

Hvis du registrerer RequestItemsService som scoped og injiserer den i to forskjellige tjenester, kan du få et element som er lagt til fra en annen tjeneste fordi de vil dele den samme RequestItemsService-forekomsten. Det er hva vi forventer av scoped-tjenester.

Men .. faktum er kanskje ikke alltid sånn. Hvis du oppretter et barneserviceareal og løser RequestItemsService fra barneomfanget, vil du få en ny forekomst av RequestItemsService og den vil ikke fungere som du forventer. Så betyr ikke scoped-tjeneste alltid forekomst per nettforespørsel.

Du tenker kanskje at du ikke gjør en så åpenbar feil (løser en scoped i et barns omfang). Men dette er ikke en feil (veldig vanlig bruk), og saken er kanskje ikke så enkel. Hvis det er en stor avhengighetsgraf mellom tjenestene dine, kan du ikke vite om noen har opprettet et barns omfang og løst en tjeneste som injiserer en annen tjeneste ... som til slutt injiserer en scoped-tjeneste.

God trening:

  • En scoped-tjeneste kan tenkes som en optimalisering der den injiseres av for mange tjenester i en nettforespørsel. Dermed vil alle disse tjenestene bruke en enkelt forekomst av tjenesten under den samme nettforespørselen.
  • Scoped-tjenester trenger ikke å være designet som trådsikker. Fordi de normalt skal brukes av en enkelt nettforespørsel / tråd. Men ... i så fall skal du ikke dele tjenestevilkår mellom forskjellige tråder!
  • Vær forsiktig hvis du designer en scoped-tjeneste for å dele data mellom andre tjenester i en nettforespørsel (forklart ovenfor). Du kan lagre data per nettforespørsel i HttpContext (injiser IHttpContextAccessor for å få tilgang til det), som er den tryggere måten å gjøre det på. HttpContext levetid er ikke scoped. Egentlig er det ikke registrert i DI i det hele tatt (det er derfor du ikke injiserer det, men injiserer IHttpContextAccessor i stedet). HttpContextAccessor-implementering bruker AsyncLocal til å dele den samme HttpContext under en nettforespørsel.

Konklusjon

Avhengighetsinjeksjon virker enkel å bruke med det første, men det er potensielle problemer med flere tråder og hukommelseslekkasjer hvis du ikke følger noen strenge prinsipper. Jeg delte noen gode prinsipper basert på mine egne erfaringer under utvikling av ASP.NET Boilerplate-rammeverket.