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

In dit artikel deel ik mijn ervaringen en suggesties over het gebruik van Dependency Injection in ASP.NET Core-applicaties. De motivatie achter deze principes is;

  • Services en hun afhankelijkheden effectief ontwerpen.
  • Multi-threading-problemen voorkomen.
  • Geheugenlekken voorkomen.
  • Potentiële bugs voorkomen.

In dit artikel wordt ervan uitgegaan dat u op een basisniveau al bekend bent met Dependency Injection en ASP.NET Core. Als dit niet het geval is, lees dan eerst de documentatie van ASP.NET Core Dependency Injection.

Basics

Constructor injectie

Constructorinjectie wordt gebruikt om afhankelijkheden van een service aan de serviceconstructie aan te geven en te verkrijgen. Voorbeeld:

openbare klasse ProductService
{
    alleen-lezen IProductRepository _productRepository;
    openbare ProductService (IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }
    public void Delete (int id)
    {
        _productRepository.Delete (id);
    }
}

ProductService injecteert IProductRepository als een afhankelijkheid in de constructor en gebruikt het vervolgens binnen de methode Delete.

Goede oefeningen:

  • Definieer vereiste afhankelijkheden expliciet in de servicebouwer. De service kan dus niet worden gebouwd zonder zijn afhankelijkheden.
  • Wijs geïnjecteerde afhankelijkheid toe aan een alleen-lezen veld / eigenschap (om te voorkomen dat er per ongeluk een andere waarde aan wordt toegewezen binnen een methode).

Eigendom injectie

De standaardafhankelijkheidsinjectiecontainer van ASP.NET Core ondersteunt geen injectie van onroerend goed. Maar u kunt een andere container gebruiken die de injectie van onroerend goed ondersteunt. Voorbeeld:

met behulp van Microsoft.Extensions.Logging;
met behulp van Microsoft.Extensions.Logging.Abstractions;
naamruimte MyApp
{
    openbare klasse ProductService
    {
        public ILogger  Logger {get; vast te stellen; }
        alleen-lezen IProductRepository _productRepository;
        openbare ProductService (IProductRepository productRepository)
        {
            _productRepository = productRepository;
            Logger = NullLogger  .Instance;
        }
        public void Delete (int id)
        {
            _productRepository.Delete (id);
            Logger.LogInformation (
                $ "Een product met id = {id} verwijderd");
        }
    }
}

ProductService declareert een Logger-eigenschap met openbare setter. Afhankelijkheid injectiecontainer kan de logger instellen als deze beschikbaar is (eerder geregistreerd bij DI-container).

Goede oefeningen:

  • Gebruik eigenschapsinjectie alleen voor optionele afhankelijkheden. Dat betekent dat uw service goed kan werken zonder deze afhankelijkheden.
  • Gebruik indien mogelijk Null-objectpatroon (zoals in dit voorbeeld). Controleer anders altijd op null terwijl u de afhankelijkheid gebruikt.

Service locator

Service locator patroon is een andere manier om afhankelijkheden te verkrijgen. Voorbeeld:

openbare klasse ProductService
{
    alleen-lezen IProductRepository _productRepository;
    alleen lezen ILogger  _logger;
    public ProductService (IServiceProvider serviceProvider)
    {
        _productRepository = serviceProvider
          .GetRequiredService  ();
        _logger = serviceProvider
          .GetService > () ??
            NullLogger  .Instance;
    }
    public void Delete (int id)
    {
        _productRepository.Delete (id);
        _logger.LogInformation ($ "Een product verwijderd met id = {id}");
    }
}

ProductService injecteert IServiceProvider en lost afhankelijkheden op met behulp ervan. GetRequiredService maakt een uitzondering als de gevraagde afhankelijkheid niet eerder is geregistreerd. Aan de andere kant retourneert GetService in dat geval gewoon null.

Wanneer u services binnen de constructor oplost, worden deze vrijgegeven wanneer de service wordt vrijgegeven. U geeft dus niet om het vrijgeven / weggooien van diensten die binnen de constructor zijn opgelost (net als de injectie van constructeurs en onroerend goed).

Goede oefeningen:

  • Gebruik het service locator-patroon niet waar mogelijk (als het type service bekend is in de ontwikkelingstijd). Omdat het de afhankelijkheden impliciet maakt. Dat betekent dat het niet mogelijk is om de afhankelijkheden gemakkelijk te zien tijdens het maken van een exemplaar van de service. Dit is vooral belangrijk voor unit-tests waarbij u mogelijk enkele afhankelijkheden van een service wilt bespotten.
  • Los afhankelijkheden in de serviceconstructeur op indien mogelijk. Het oplossen in een servicemethode maakt uw applicatie ingewikkelder en foutgevoeliger. Ik zal de problemen en oplossingen in de volgende paragrafen behandelen.

Levensduur

Er zijn drie servicelevensduren in ASP.NET Core Dependency Injection:

  1. Tijdelijke services worden gemaakt telkens wanneer ze worden geïnjecteerd of aangevraagd.
  2. Scoped-services worden per scope gemaakt. In een webtoepassing creëert elk webverzoek een nieuw gescheiden servicebereik. Dat betekent dat scoped-services meestal per webverzoek worden gemaakt.
  3. Singleton-services worden gemaakt per DI-container. Dat betekent in het algemeen dat ze slechts één keer per applicatie worden gemaakt en vervolgens de hele levensduur van de applicatie worden gebruikt.

DI-container houdt alle opgeloste services bij. Services worden vrijgegeven en verwijderd wanneer hun levensduur eindigt:

  • Als de service afhankelijkheden heeft, worden deze ook automatisch vrijgegeven en verwijderd.
  • Als de service de IDisposable-interface implementeert, wordt de verwijderingsmethode automatisch aangeroepen bij het vrijgeven van de service.

Goede oefeningen:

  • Registreer uw services waar mogelijk als tijdelijk. Omdat het eenvoudig is om tijdelijke services te ontwerpen. Over het algemeen geeft u niet om multi-threading en geheugenlekken en u weet dat de service een korte levensduur heeft.
  • Gebruik de levensduur van scoped-services zorgvuldig, want het kan lastig zijn als u scopes voor kindservices maakt of deze services vanuit een niet-webtoepassing gebruikt.
  • Gebruik singleton life zorgvuldig sindsdien moet je omgaan met multi-threading en mogelijke geheugenlekproblemen.
  • Vertrouw niet op een tijdelijke of scoped-service van een singleton-service. Omdat de tijdelijke service een singleton-instantie wordt wanneer een singleton-service deze injecteert en dat kan problemen veroorzaken als de tijdelijke service niet is ontworpen om een ​​dergelijk scenario te ondersteunen. De standaard DI-container van ASP.NET Core maakt in dergelijke gevallen al uitzonderingen.

Services oplossen in een methode-instantie

In sommige gevallen moet u mogelijk een andere service oplossen via een methode van uw service. Zorg er in dergelijke gevallen voor dat u de service na gebruik vrijgeeft. De beste manier om dat te verzekeren, is om een ​​servicescope te creëren. Voorbeeld:

openbare klasse PriceCalculator
{
    alleen-lezen IServiceProvider _serviceProvider;
    openbare PriceCalculator (IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    public float Bereken (Productproduct, int. aantal,
      Type taxStrategyServiceType)
    {
        using (var scope = _serviceProvider.CreateScope ())
        {
            var taxStrategy = (ITaxStrategy) scope.ServiceProvider
              .GetRequiredService (taxStrategyServiceType);
            var prijs = product. prijs * aantal;
            retourprijs + btwStrategy.CalculateTax (prijs);
        }
    }
}

PriceCalculator injecteert de IServiceProvider in de constructor en wijst deze toe aan een veld. PriceCalculator gebruikt het vervolgens in de berekeningsmethode om een ​​kindservicebereik te maken. Het gebruikt scope.ServiceProvider om services op te lossen, in plaats van de geïnjecteerde _serviceProvider-instantie. Zo worden alle services die uit de scope zijn opgelost, automatisch vrijgegeven / verwijderd aan het einde van de gebruiksverklaring.

Goede oefeningen:

  • Als u een service in een methodelichaam oplost, moet u altijd een kinderservicebereik maken om ervoor te zorgen dat de opgeloste services correct worden vrijgegeven.
  • Als een methode IServiceProvider als argument krijgt, kunt u de services ervan direct oplossen zonder zich zorgen te maken over het vrijgeven / weggooien. Het creëren / beheren van servicebereik is een verantwoordelijkheid van de code die uw methode aanroept. Door dit principe te volgen, wordt uw code schoner.
  • Houd geen verwijzing naar een opgeloste service! Anders kan dit geheugenlekken veroorzaken en hebt u toegang tot een verwijderde service wanneer u de objectreferentie later gebruikt (tenzij de opgeloste service singleton is).

Singleton-services

Singleton-services zijn meestal ontworpen om de status van een applicatie te behouden. Een cache is een goed voorbeeld van applicatiestatussen. Voorbeeld:

openbare klasse FileService
{
    alleen-lezen ConcurrentDictionary  _cache;
    openbare FileService ()
    {
        _cache = nieuw ConcurrentDictionary  ();
    }
    public byte [] GetFileContent (string filePath)
    {
        retourneer _cache.GetOrAdd (filePath, _ =>
        {
            terugkeer File.ReadAllBytes (filePath);
        });
    }
}

FileService slaat de inhoud van bestanden eenvoudig op in het cachegeheugen om schijflezingen te verminderen. Deze service moet worden geregistreerd als singleton. Anders werkt caching niet zoals verwacht.

Goede oefeningen:

  • Als de service een status heeft, moet deze toegang hebben tot die status op een threadveilige manier. Omdat alle aanvragen tegelijkertijd hetzelfde exemplaar van de service gebruiken. Ik gebruikte ConcurrentDictionary in plaats van Dictionary om de veiligheid van de threads te waarborgen.
  • Gebruik geen scoped- of tijdelijke services van singleton-services. Omdat tijdelijke services mogelijk niet zijn ontworpen om thread-thread te zijn. Als u ze moet gebruiken, zorg dan voor multi-threading terwijl u deze services gebruikt (gebruik bijvoorbeeld lock).
  • Geheugenlekken worden meestal veroorzaakt door singleton-services. Ze worden niet vrijgegeven / verwijderd tot het einde van de aanvraag. Dus als ze klassen instantiëren (of injecteren) maar ze niet vrijgeven / weggooien, blijven ze ook in het geheugen tot het einde van de toepassing. Zorg ervoor dat u ze op het juiste moment vrijgeeft / weggooit. Zie de sectie Services oplossen in een methode Body hierboven.
  • Als u gegevens in de cache opslaat (in dit voorbeeld de bestandsinhoud), moet u een mechanisme maken om de gegevens in het cachegeheugen bij te werken / ongeldig te maken wanneer de oorspronkelijke gegevensbron wijzigt (wanneer een cachebestand voor dit voorbeeld op de schijf wordt gewijzigd).

Scoped Services

Scoped-levensduur lijkt eerst een goede kandidaat om gegevens per webaanvraag op te slaan. Omdat ASP.NET Core een servicescope creëert per webverzoek. Dus als u een service als Scoped registreert, kan deze tijdens een webverzoek worden gedeeld. Voorbeeld:

openbare klasse RequestItemsService
{
    alleen-lezen woordenboek  _items;
    public RequestItemsService ()
    {
        _items = nieuw woordenboek  ();
    }
    public void Set (stringnaam, objectwaarde)
    {
        _items [name] = waarde;
    }
    openbaar object Get (tekenreeksnaam)
    {
        retourneer _items [naam];
    }
}

Als u de RequestItemsService registreert als scoped en deze in twee verschillende services injecteert, kunt u een item krijgen dat wordt toegevoegd vanuit een andere service omdat deze dezelfde RequestItemsService-instantie delen. Dat is wat we van scoped services verwachten.

Maar ... het feit is misschien niet altijd zo. Als u een kindservicebereik maakt en de RequestItemsService uit het kindbereik oplost, krijgt u een nieuw exemplaar van de RequestItemsService en werkt deze niet zoals u verwacht. Scoped-service betekent dus niet altijd exemplaar per webverzoek.

Je denkt misschien dat je niet zo'n voor de hand liggende fout maakt (het oplossen van een scoped binnen een kindscope). Maar dit is geen fout (een zeer regelmatig gebruik) en de zaak is misschien niet zo eenvoudig. Als er een grote afhankelijkheidsgrafiek is tussen uw services, kunt u niet weten of iemand een kindscope heeft gemaakt en een service heeft opgelost die een andere service injecteert ... die uiteindelijk een scoped-service injecteert.

Goede praktijk:

  • Een scoped-service kan worden beschouwd als een optimalisatie waarbij deze door te veel services in een webaanvraag wordt geïnjecteerd. Al deze services zullen dus één enkel exemplaar van de service gebruiken tijdens hetzelfde webverzoek.
  • Scoped-services hoeven niet te zijn ontworpen als thread-safe. Omdat ze normaal gesproken moeten worden gebruikt door een enkele webaanvraag / thread. Maar ... in dat geval moet u geen servicescopes delen tussen verschillende threads!
  • Wees voorzichtig als u een scoped-service ontwerpt om gegevens te delen tussen andere services in een webverzoek (hierboven uitgelegd). U kunt gegevens per webverzoek opslaan in de HttpContext (IHttpContextAccessor injecteren om er toegang toe te krijgen), wat de veiligere manier is om dat te doen. De levensduur van HttpContext is niet scoped. Eigenlijk is het helemaal niet geregistreerd bij DI (daarom injecteer je het niet, maar injecteer ik in plaats daarvan IHttpContextAccessor). HttpContextAccessor-implementatie gebruikt AsyncLocal om dezelfde HttpContext te delen tijdens een webaanvraag.

Gevolgtrekking

Afhankelijkheidsinjectie lijkt aanvankelijk eenvoudig te gebruiken, maar er zijn potentiële problemen met multi-threading en geheugenlekken als u zich niet aan enkele strikte principes houdt. Ik heb een aantal goede principes gedeeld op basis van mijn eigen ervaringen tijdens de ontwikkeling van het ASP.NET Boilerplate-framework.