ASP.NET Core Dependency Injection bedste praksis, tip og tricks

I denne artikel vil jeg dele mine erfaringer og forslag til anvendelse af afhængighedsinjektion i ASP.NET Core-applikationer. Motivationen bag disse principper er;

  • Effektiv design af tjenester og deres afhængighed.
  • Forebyggelse af problemer med flere tråde.
  • Forebyggelse af hukommelseslækager.
  • Forebyggelse af potentielle fejl.

Denne artikel antager, at du allerede er bekendt med Dependency Injection og ASP.NET Core på et grundlæggende niveau. Hvis ikke, skal du først læse dokumentationen til ASP.NET Core Dependency Injection.

Grundlæggende

Konstruktørinjektion

Konstruktørinjektion bruges til at erklære og opnå afhængigheder af en service på servicekonstruktionen. Eksempel:

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

ProductService injicerer IProductRepository som en afhængighed i sin konstruktør og bruger det derefter i Delete-metoden.

God praksis:

  • Definer de nødvendige afhængigheder eksplicit i servicekonstruktøren. Tjenesten kan således ikke konstrueres uden dens afhængigheder.
  • Tildel den indsprøjtede afhængighed til et skrivebeskyttet felt / egenskab (for at forhindre, at der tildeles en anden værdi til det i en metode).

Indsprøjtning af ejendom

ASP.NET Core's standardindsprøjtningsindsprøjtningsbeholder understøtter ikke egenskabsinjektion. Men du kan bruge en anden container, der understøtter ejendomsinjektionen. Eksempel:

ved hjælp af Microsoft.Extensions.Logging;
ved hjælp af Microsoft.Extensions.Logging.Abstraktioner;
navneområde MyApp
{
    offentlig klasse ProductService
    {
        public ILogger  Logger {get; sæt; }
        privat readonly IProductRepository _productRepository;
        public ProductService (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger .
        }
        public void Delete (int id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Slet et produkt med id = {id}");
        }
    }
}

ProductService erklærer en Logger-ejendom hos den offentlige sæter. Afhængighedsinjektionsbeholder kan indstille Loggeren, hvis den er tilgængelig (registreret i DI-beholder før).

God praksis:

  • Brug kun egenskabsinjektion til valgfri afhængighed. Det betyder, at din service kan fungere korrekt uden disse leverede afhængigheder.
  • Brug Null Object Pattern (som i dette eksempel), hvis det er muligt. Ellers skal du altid kontrollere for null, mens du bruger afhængigheden.

Service Locator

Servicelokatormønster er en anden måde at få afhængigheder på. Eksempel:

offentlig klasse ProductService
{
    privat 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 ($ "Slet et produkt med id = {id}");
    }
}

ProductService injicerer IServiceProvider og løser afhængigheder ved hjælp af det. GetRequiredService kaster undtagelse, hvis den anmodede afhængighed ikke var registreret før. På den anden side returnerer GetService bare null i dette tilfælde.

Når du løser tjenester inde i konstruktøren, frigives de, når tjenesten frigives. Så du er ligeglad med at frigive / bortskaffe tjenester, der er løst inde i konstruktøren (ligesom konstruktør og ejendom indsprøjtning).

God praksis:

  • Brug ikke servicelokalitetsmønsteret, hvor det er muligt (hvis servicetypen er kendt i udviklingstiden). Fordi det gør afhængighederne implicit. Det betyder, at det ikke er muligt at se afhængighederne let, mens du opretter en forekomst af tjenesten. Dette er især vigtigt for enhedstest, hvor du måske vil håne nogle afhængigheder af en tjeneste.
  • Løs afhængigheder i servicekonstruktøren, hvis det er muligt. Løsning i en servicemetode gør din applikation mere kompliceret og har en tilbøjelig fejl. Jeg vil dække problemer og løsninger i de næste afsnit.

Servicelivstider

Der er tre levetider i ASP.NET Core Dependency Injection:

  1. Forbigående tjenester oprettes hver gang de injiceres eller anmodes om.
  2. Scoped-tjenester oprettes pr. Omfang. I en webapplikation opretter hver webanmodning et nyt adskilt serviceareal. Det betyder, at scoped-tjenester generelt oprettes pr. Webanmodning.
  3. Singleton-tjenester oprettes pr. DI-container. Det betyder generelt, at de kun oprettes en gang pr. Applikation og derefter bruges til hele applikationens levetid.

DI container holder styr på alle løste tjenester. Tjenester frigives og bortskaffes, når deres levetid slutter:

  • Hvis tjenesten har afhængigheder, frigives de også automatisk og bortskaffes.
  • Hvis tjenesten implementerer den identificerbare grænseflade, kaldes Bortskaffelsesmetoden automatisk til servicefrigivelse.

God praksis:

  • Registrer dine tjenester som kortvarige, hvor det er muligt. Fordi det er nemt at designe forbigående tjenester. Du er normalt ikke interesseret i multi-threading og hukommelses lækager, og du ved, at tjenesten har en kort levetid.
  • Brug levetid for scoped-service omhyggeligt, da det kan være vanskeligt, hvis du opretter børneserviceaftaler eller bruger disse tjenester fra en ikke-webapplikation.
  • Brug singleton levetid omhyggeligt, da du har brug for at tackle multi-threading og potentielle hukommelses lækager problemer.
  • Ikke afhængig af en forbigående eller scoped-service fra en singleton-tjeneste. Fordi den forbigående tjeneste bliver en singleton-forekomst, når en singleton-tjeneste indsprøjter den, og det kan forårsage problemer, hvis den forbigående tjeneste ikke er designet til at understøtte et sådant scenario. ASP.NET Core's standard DI-container kaster allerede undtagelser i sådanne tilfælde.

Løsning af tjenester i en metodekrop

I nogle tilfælde skal du muligvis løse en anden tjeneste i en metode til din tjeneste. I sådanne tilfælde skal du sikre dig, at du frigiver tjenesten efter brug. Den bedste måde at sikre det på er at skabe et serviceomfang. Eksempel:

offentlig klasse PriceCalculator
{
    privat readonly IServiceProvider _serviceProvider;
    public PriceCalculator (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    public float Beregn (Produktprodukt, antal tællinger,
      Skriv skatStrategyServiceType)
    {
        bruger (var scope = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) scope.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            var pris = produkt. Pris * tæller;
            returpris + skatStrategi. Beregn pris (pris);
        }
    }
}

PriceCalculator indsprøjter IServiceProvider i sin konstruktør og tildeler den til et felt. PriceCalculator bruger det derefter i beregningsmetoden til at skabe et børneserviceafdeling. Det bruger scope.ServiceProvider til at løse tjenester i stedet for den injicerede _serviceProvider-forekomst. Således frigøres / bortskaffes alle tjenester, der løses fra omfanget, automatisk efter afslutningen af ​​brugserklæringen.

God praksis:

  • Hvis du løser en tjeneste i en metodekrop, skal du altid oprette et børneserviceforhold for at sikre, at de løste tjenester frigives korrekt.
  • Hvis en metode får IServiceProvider som et argument, kan du direkte løse tjenester fra den uden at være opmærksom på frigivelse / bortskaffelse. Oprettelse / styring af tjenesteomfang er et ansvar for den kode, der kalder din metode. Ved at følge dette princip gør din kode renere.
  • Henvis ikke til en løst service! Ellers kan det forårsage hukommelseslækager, og du får adgang til en bortskaffet tjeneste, når du bruger objektreferencen senere (medmindre den løste service er singleton).

Singleton Services

Singleton-tjenester er generelt designet til at bevare en ansøgningstilstand. En cache er et godt eksempel på applikationstilstande. Eksempel:

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

FileService cachen blot filindhold for at reducere disklæsninger. Denne service skal registreres som singleton. Ellers fungerer cache ikke som forventet.

God praksis:

  • Hvis tjenesten har en tilstand, skal den få adgang til denne tilstand på en tråd-sikker måde. Fordi alle anmodninger samtidigt bruger den samme forekomst af tjenesten. Jeg brugte ConcurrentDictions i stedet for Dictionary for at sikre tråd sikkerhed.
  • Brug ikke scoped- eller forbigående tjenester fra singleton-tjenester. Fordi forbigående tjenester muligvis ikke er designet til at være tråd-sikre. Hvis du skal bruge dem, skal du passe på multi-threading, mens du bruger disse tjenester (brug f.eks. Lås).
  • Hukommelseslækager skyldes generelt singleton-tjenester. De frigives / bortskaffes først inden afslutningen af ​​ansøgningen. Så hvis de instantierer klasser (eller sprøjter ind), men ikke frigiver / bortskaffer dem, forbliver de også i hukommelsen, indtil applikationens afslutning. Sørg for, at du frigiver / bortskaffer dem på det rigtige tidspunkt. Se Løsningstjenester i et afsnit om metodekroppe ovenfor.
  • Hvis du cache-data (filindhold i dette eksempel), skal du oprette en mekanisme til at opdatere / ugyldige cache-data, når den originale datakilde ændres (når en cache-fil ændres på disken i dette eksempel).

Scoped Services

Scoped levetid forekommer først en god kandidat til at gemme pr. Webanmodningsdata. Fordi ASP.NET Core skaber et serviceforhold per webanmodning. Så hvis du registrerer en tjeneste som scoped, kan den deles under en webanmodning. Eksempel:

offentlig klasse RequestItemsService
{
    privat readonly ordbog  _items;
    public RequestItemsService ()
    {
        _items = nyt ordbog  ();
    }
    public void Set (strengnavn, objektværdi)
    {
        _items [name] = værdi;
    }
    offentligt objekt Få (strengnavn)
    {
        return _items [name];
    }
}

Hvis du registrerer RequestItemsService som scoped og injicerer den i to forskellige tjenester, kan du få et emne, der er tilføjet fra en anden service, fordi de vil dele den samme RequestItemsService-forekomst. Det er hvad vi forventer af scoped-tjenester.

Men .. faktum er måske ikke altid sådan. Hvis du opretter et børneserviceforhold og løser RequestItemsService fra underområdet, får du en ny forekomst af RequestItemsService, og den fungerer ikke, som du forventer. Så betyder scoped-service ikke altid forekomst pr. Webanmodning.

Du tror måske, at du ikke begår en så åbenlyst fejltagelse (at løse en scoped inden for et børns rækkevidde). Men dette er ikke en fejl (en meget regelmæssig anvendelse), og sagen er muligvis ikke så enkel. Hvis der er en stor afhængighedsgrafik mellem dine tjenester, kan du ikke vide, om nogen skabte et børns rækkevidde og løste en tjeneste, der indsprøjter en anden service ... som til sidst indsprøjter en scoped-tjeneste.

God øvelse:

  • En scoped-service kan betragtes som en optimering, hvor den indsprøjtes af for mange tjenester i en webanmodning. Således bruger alle disse tjenester en enkelt forekomst af tjenesten under den samme webanmodning.
  • Scoped-tjenester behøver ikke at blive designet som tråd-sikkert. Fordi de normalt skal bruges af en enkelt webanmodning / tråd. Men ... i så fald bør du ikke dele serviceafgrænsninger mellem forskellige tråde!
  • Vær forsigtig, hvis du designer en scoped-tjeneste til at dele data mellem andre tjenester i en webanmodning (forklaret ovenfor). Du kan gemme pr. Webanmodningsdata inde i HttpContext (injicere IHttpContextAccessor for at få adgang til det), hvilket er den sikrere måde at gøre det på. HttpContext levetid er ikke scoped. Faktisk er det overhovedet ikke registreret til DI (det er derfor, du ikke injicerer det, men injicerer IHttpContextAccessor i stedet). HttpContextAccessor-implementering bruger AsyncLocal til at dele den samme HttpContext under en webanmodning.

Konklusion

Afhængighedsinjektion virker enkel at bruge til at begynde med, men der er potentielle problemer med flere tråde og hukommelseslækage, hvis du ikke følger nogle strenge principper. Jeg delte nogle gode principper baseret på mine egne oplevelser under udvikling af ASP.NET Boilerplate-rammen.