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

I den här artikeln kommer jag att dela mina erfarenheter och förslag om att använda Dependency Injection i ASP.NET Core-applikationer. Motivationen bakom dessa principer är;

  • Designa effektiva tjänster och deras beroenden.
  • Förhindrar problem med flera trådar.
  • Förhindrar minnesläckage.
  • Förhindra potentiella buggar.

Den här artikeln antar att du redan är bekant med Dependency Injection och ASP.NET Core på en grundläggande nivå. Om inte, läs först ASP.NET Core Dependency Injection-dokumentationen.

Grunderna

Constructor Injection

Konstruktörsprutning används för att förklara och erhålla beroende av en tjänst på servicekonstruktionen. Exempel:

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

ProductService injicerar IProductRepository som ett beroende i sin konstruktör och använder den sedan i Delete-metoden.

Bra övningar:

  • Definiera nödvändiga beroenden uttryckligen i servicekonstruktören. Således kan tjänsten inte byggas utan dess beroenden.
  • Tilldela injicerat beroende till ett läsbart fält / egenskap (för att förhindra att ett annat värde tilldelas ett annat värde i en metod).

Fastighetsinsprutning

ASP.NET Core: s standardbehandlingsinjektionsbehållare stöder inte injektion av egendom. Men du kan använda en annan behållare som stöder fastighetsinsprutningen. Exempel:

använder Microsoft.Extensions.Logging;
använder Microsoft.Extensions.Logging.Abstraktioner;
namnutrymme MyApp
{
    offentlig klass ProductService
    {
        public ILogger  Logger {get; uppsättning; }
        privat readonly IProductRepository _productRepository;
        public ProductService (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger .
        }
        public void Delete (int id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Raderade en produkt med id = {id}");
        }
    }
}

ProductService deklarerar en Logger-egendom med den offentliga säljaren. Behållarinjektionsbehållare kan ställa in loggen om den är tillgänglig (registrerad i DI-behållaren innan).

Bra övningar:

  • Använd endast injektionsegenskaper för valfria beroenden. Det betyder att din tjänst kan fungera ordentligt utan dessa beroenden.
  • Använd nollobjektmönster (som i det här exemplet) om möjligt. Annars ska du alltid leta efter noll medan du använder beroendet.

Service Locator

Service locator-mönster är ett annat sätt att få beroenden. Exempel:

offentlig klass 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 ($ "Raderade en produkt med id = {id}");
    }
}

ProductService injicerar IServiceProvider och löser beroenden med den. GetRequiredService kastar undantag om det begärda beroendet inte registrerades tidigare. Å andra sidan returnerar GetService bara noll i så fall.

När du löser tjänster i konstruktören släpps de när tjänsten släpps. Så du bryr dig inte om att släppa / bortskaffa tjänster som är löst inuti konstruktören (precis som konstruktör och fastighetsinsprutning).

Bra övningar:

  • Använd inte servicelokalitetsmönstret där det är möjligt (om servicetypen är känd under utvecklingstiden). Eftersom det gör beroenden implicit. Det betyder att det inte är möjligt att se beroenden enkelt medan du skapar en instans av tjänsten. Detta är särskilt viktigt för enhetstester där du kanske vill håna vissa beroende av en tjänst.
  • Lös eventuella beroende i servicekonstruktören. Att lösa i en servicemetod gör din applikation mer komplicerad och felaktig. Jag kommer att täcka problemen och lösningarna i nästa avsnitt.

Service Life Times

Det finns tre livslängder i ASP.NET Core Dependency Injection:

  1. Övergående tjänster skapas varje gång de injiceras eller begärs.
  2. Skopatjänster skapas per omfattning. I en webbapplikation skapar varje webbförfrågan ett nytt separerat tjänstefält. Det innebär att scopedtjänster generellt skapas per webbbegäran.
  3. Singleton-tjänster skapas per DI-container. Det innebär generellt att de bara skapas en gång per applikation och sedan används för hela applikationens livstid.

DI-container håller reda på alla lösta tjänster. Tjänster släpps och bortskaffas när deras livstid slutar:

  • Om tjänsten har beroenden släpps de också automatiskt och bortskaffas.
  • Om tjänsten implementerar IDisposable-gränssnittet kallas Dispose-metoden automatiskt vid serviceutgåvan.

Bra övningar:

  • Registrera dina tjänster som övergående där så är möjligt. Eftersom det är enkelt att designa övergående tjänster. Du bryr dig i allmänhet inte om flera trådar och minnesläckor och du vet att tjänsten har en kort livslängd.
  • Använd försiktigt livslängd för scoped-tjänster eftersom det kan vara svårt om du skapar barntjänstområden eller använder dessa tjänster från en icke-webbapplikation.
  • Använd singletons livstid noggrant sedan du måste hantera problem med flera gängor och potentiella minnesläckor.
  • Förlita dig inte på en övergående eller scoped-tjänst från en singleton-tjänst. Eftersom den övergående tjänsten blir en singleton-instans när en singleton-tjänst injicerar den och det kan orsaka problem om den övergående tjänsten inte är utformad för att stödja ett sådant scenario. ASP.NET Core standard DI-behållare kastar redan undantag i sådana fall.

Lösa tjänster i ett metodorgan

I vissa fall kan du behöva lösa en annan tjänst i en metod för din tjänst. Se i sådana fall till att du släpper tjänsten efter användning. Det bästa sättet att säkerställa att det är att skapa ett servicealternativ. Exempel:

offentlig klass PriceCalculator
{
    privat readonly IServiceProvider _serviceProvider;
    public PriceCalculator (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    public float Beräkna (produktprodukt, int-räkning,
      Skriv skattStrategyServiceType)
    {
        använder (var scope = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) scope.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            var pris = produkt. Pris * räknas;
            returpris + skattStrategi. Beräkna taxa (pris);
        }
    }
}

PriceCalculator injicerar IServiceProvider i sin konstruktör och tilldelar den till ett fält. PriceCalculator använder det sedan i beräkningsmetoden för att skapa ett barntjänstområde. Den använder scope.ServiceProvider för att lösa tjänster istället för den injicerade instansen _serviceProvider. Därför släpps / bortskaffas alla tjänster som löses från räckvidden automatiskt i slutet av det använda uttalandet.

Bra övningar:

  • Om du löser en tjänst i ett metodorgan skapar du alltid ett barntjänstområde för att se till att de lösade tjänsterna släpps korrekt.
  • Om en metod får IServiceProvider som ett argument kan du direkt lösa tjänster från det utan att bry dig om att släppa / avyttra. Att skapa / hantera tjänsteomfång är ett ansvar för koden som kallar din metod. Genom att följa denna princip gör din kod renare.
  • Håll inte en referens till en löst tjänst! Annars kan det orsaka minnesläckor och du kommer åt en avyttrad tjänst när du använder objektreferensen senare (såvida inte den lösta tjänsten är singleton).

Singleton Services

Singleton-tjänster är generellt utformade för att behålla en applikationstillstånd. En cache är ett bra exempel på applikationstillstånd. Exempel:

FileService för allmän klass
{
    privat readonly ConcurrentDiktion  _cache;
    public FileService ()
    {
        _cache = new ConcurrentDictionary  ();
    }
    public byte [] GetFileContent (string filePath)
    {
        returnera _cache.GetOrAdd (filePath, _ =>
        {
            returnera File.ReadAllBytes (filePath);
        });
    }
}

FileService cachar helt enkelt filinnehållet för att minska skivläsningarna. Denna tjänst bör registreras som singleton. Annars fungerar cachningen inte som förväntat.

Bra övningar:

  • Om tjänsten har ett tillstånd bör den få åtkomst till det tillståndet på ett tråd-säkert sätt. Eftersom alla förfrågningar samtidigt använder samma instans av tjänsten. Jag använde ConcurrentDiction istället för Dictionary för att säkerställa trådens säkerhet.
  • Använd inte scoped- eller övergående tjänster från singleton-tjänster. Eftersom transienttjänster kanske inte är utformade för att vara trådsäkra. Om du måste använda dem, ta hand om flera trådar när du använder dessa tjänster (använd lås till exempel).
  • Minnesläckor orsakas vanligtvis av singleton-tjänster. De släpps inte / bortskaffas förrän applikationens slut. Så om de initierar klasser (eller injicerar) men inte släpper / kasserar dem, kommer de också att stanna kvar i minnet tills programmets slut. Se till att du släpper / kasserar dem vid rätt tidpunkt. Se avsnittet Lösningstjänster i en metodkropp ovan.
  • Om du cachar data (filinnehållet i det här exemplet) bör du skapa en mekanism för att uppdatera / ogiltiga cachardata när den ursprungliga datakällan ändras (när en cache-fil ändras på disken i det här exemplet).

Scoped Services

Scoped livstid verkar först vara en bra kandidat för att lagra per data på webben. Eftersom ASP.NET Core skapar ett tjänsteutrymme per webbförfrågan. Så om du registrerar en tjänst som scoped kan den delas under en webbbegäran. Exempel:

offentliga klass RequestItemsService
{
    privat readonly Dictionary  _items;
    public RequestItemsService ()
    {
        _items = new Dictionary  ();
    }
    public void Set (strängnamn, objektvärde)
    {
        _items [name] = värde;
    }
    offentligt objekt Få (strängnamn)
    {
        return _items [name];
    }
}

Om du registrerar RequestItemsService som scoped och injicerar den i två olika tjänster kan du få ett objekt som läggs till från en annan tjänst eftersom de delar samma RequestItemsService-instans. Det är vad vi förväntar oss av scoped-tjänster.

Men .. faktum är kanske inte alltid så. Om du skapar ett barntjänstområde och löser RequestItemsService från barnomfånget får du en ny instans av RequestItemsService och den fungerar inte som du förväntar dig. Så betyder scoped-tjänst inte alltid instans per webbbegäran.

Du kanske tror att du inte gör ett så uppenbart misstag (lösa en scoped inom ett barnomfång). Men detta är inte ett misstag (en mycket regelbunden användning) och fallet kanske inte är så enkelt. Om det finns ett stort beroende-diagram mellan dina tjänster, kan du inte veta om någon skapade ett barnomfång och löste en tjänst som injicerar en annan tjänst ... som till slut injicerar en scoped-tjänst.

Bra övning:

  • En scoped-tjänst kan ses som en optimering där den injiceras av för många tjänster i en webbegäran. Således kommer alla dessa tjänster att använda en enda instans av tjänsten under samma webbbegäran.
  • Scopedtjänster behöver inte utformas som trådsäkra. Eftersom de normalt bör användas av en enda webbegäran / tråd. Men ... i så fall ska du inte dela serviceavstånd mellan olika trådar!
  • Var försiktig om du utformar en scoped-tjänst för att dela data mellan andra tjänster i en webbbegäran (förklaras ovan). Du kan lagra data per webbförfrågan inuti HttpContext (injicera IHttpContextAccessor för att få åtkomst till dem) vilket är det säkrare sättet att göra det. HttpContext livslängd är inte scoped. Egentligen är det inte registrerat på DI alls (det är därför du inte injicerar det utan injicerar IHttpContextAccessor istället). HttpContextAccessor-implementering använder AsyncLocal för att dela samma HttpContext under en webbbegäran.

Slutsats

Beroende på injektion verkar enkelt att använda till en början, men det finns potentiella problem med flera trådar och minnesläckage om du inte följer några strikta principer. Jag delade några bra principer baserade på mina egna erfarenheter under utvecklingen av ASP.NET Boilerplate-ramverket.