Richtlijnen voor afhankelijkheidsinjectie

Dit artikel bevat algemene richtlijnen en aanbevolen procedures voor het implementeren van afhankelijkheidsinjectie in .NET-toepassingen.

Services ontwerpen voor afhankelijkheidsinjectie

Bij het ontwerpen van services voor afhankelijkheidsinjectie:

  • Vermijd stateful, statische klassen en leden. Vermijd het maken van een globale status door apps te ontwerpen om in plaats daarvan singleton-services te gebruiken.
  • Vermijd directe instantiëring van afhankelijke klassen binnen services. Directe instantiëring koppelt de code aan een bepaalde implementatie.
  • Maak services klein, goed gefactoreerd en eenvoudig getest.

Als een klasse veel geïnjecteerde afhankelijkheden heeft, kan het een teken zijn dat de klasse te veel verantwoordelijkheden heeft en het SRP (Single Responsibility Principle) schendt. Probeer de klasse te herstructureren door een deel van de verantwoordelijkheden naar nieuwe klassen te verplaatsen.

Verwijdering van diensten

De container is verantwoordelijk voor het opschonen van typen die worden gemaakt en roept exemplaren Dispose aan IDisposable . Services die zijn omgezet vanuit de container, mogen nooit worden verwijderd door de ontwikkelaar. Als een type of factory is geregistreerd als een singleton, wordt de singleton automatisch verwijderd door de container.

In het volgende voorbeeld worden de services gemaakt door de servicecontainer en automatisch verwijderd:

namespace ConsoleDisposable.Example;

public sealed class TransientDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(TransientDisposable)}.Dispose()");
}

Het voorgaande wegwerp is bedoeld om een tijdelijke levensduur te hebben.

namespace ConsoleDisposable.Example;

public sealed class ScopedDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(ScopedDisposable)}.Dispose()");
}

Het voorgaande wegwerp is bedoeld om een bereik van levensduur te hebben.

namespace ConsoleDisposable.Example;

public sealed class SingletonDisposable : IDisposable
{
    public void Dispose() => Console.WriteLine($"{nameof(SingletonDisposable)}.Dispose()");
}

Het voorgaande wegwerp is bedoeld om een singleton-levensduur te hebben.

using ConsoleDisposable.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddTransient<TransientDisposable>();
builder.Services.AddScoped<ScopedDisposable>();
builder.Services.AddSingleton<SingletonDisposable>();

using IHost host = builder.Build();

ExemplifyDisposableScoping(host.Services, "Scope 1");
Console.WriteLine();

ExemplifyDisposableScoping(host.Services, "Scope 2");
Console.WriteLine();

await host.RunAsync();

static void ExemplifyDisposableScoping(IServiceProvider services, string scope)
{
    Console.WriteLine($"{scope}...");

    using IServiceScope serviceScope = services.CreateScope();
    IServiceProvider provider = serviceScope.ServiceProvider;

    _ = provider.GetRequiredService<TransientDisposable>();
    _ = provider.GetRequiredService<ScopedDisposable>();
    _ = provider.GetRequiredService<SingletonDisposable>();
}

In de console voor foutopsporing ziet u de volgende voorbeelduitvoer nadat deze is uitgevoerd:

Scope 1...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

Scope 2...
ScopedDisposable.Dispose()
TransientDisposable.Dispose()

info: Microsoft.Hosting.Lifetime[0]
      Application started.Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
     Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
     Content root path: .\configuration\console-di-disposable\bin\Debug\net5.0
info: Microsoft.Hosting.Lifetime[0]
     Application is shutting down...
SingletonDisposable.Dispose()

Services die niet zijn gemaakt door de servicecontainer

Kijk eens naar de volgende code:

// Register example service in IServiceCollection
builder.Services.AddSingleton(new ExampleService());

In de voorgaande code:

  • Het ExampleService exemplaar wordt niet gemaakt door de servicecontainer.
  • In het framework worden de services niet automatisch verwijderd.
  • De ontwikkelaar is verantwoordelijk voor het verwijderen van de services.

IDisposable-richtlijnen voor tijdelijke en gedeelde exemplaren

Tijdelijke, beperkte levensduur

Scenario

De app vereist een IDisposable exemplaar met een tijdelijke levensduur voor een van de volgende scenario's:

  • Het exemplaar wordt omgezet in het hoofdbereik (hoofdcontainer).
  • Het exemplaar moet worden verwijderd voordat het bereik afloopt.

Oplossing

Gebruik het factory-patroon om een exemplaar buiten het bovenliggende bereik te maken. In deze situatie zou de app over het algemeen een Create methode hebben waarmee de constructor van het uiteindelijke type rechtstreeks wordt aangeroepen. Als het laatste type andere afhankelijkheden heeft, kan de factory het volgende doen:

Gedeeld exemplaar, beperkte levensduur

Scenario

De app vereist een gedeeld exemplaar IDisposable voor meerdere services, maar het IDisposable exemplaar moet een beperkte levensduur hebben.

Oplossing

Registreer het exemplaar met een levensduur binnen het bereik. Gebruik IServiceScopeFactory.CreateScope dit om een nieuwe IServiceScopete maken. Gebruik het bereik IServiceProvider om vereiste services te verkrijgen. Verwijder het bereik wanneer dit niet meer nodig is.

Algemene IDisposable richtlijnen

  • Registreer IDisposable geen exemplaren met een tijdelijke levensduur. Gebruik in plaats daarvan het fabriekspatroon.
  • Los exemplaren met een tijdelijke of scoped levensduur niet op IDisposable in het hoofdbereik. De enige uitzondering hierop is als de app maakt/opnieuw maakt en verwijdert IServiceProvider, maar dit is geen ideaal patroon.
  • Voor het ontvangen van een IDisposable afhankelijkheid via DI is niet vereist dat de ontvanger zichzelf implementeert IDisposable . De ontvanger van de IDisposable afhankelijkheid mag die afhankelijkheid niet aanroepen Dispose .
  • Gebruik bereiken om de levensduur van services te beheren. Bereiken zijn niet hiërarchisch en er is geen speciale verbinding tussen bereiken.

Zie Een methode implementeren Dispose of een methode implementeren voor meer informatie over het opschonen van resourcesDisposeAsync. Houd ook rekening met de tijdelijke wegwerpservices die zijn vastgelegd in het containerscenario , omdat het betrekking heeft op het opschonen van resources.

Standaardservicecontainervervanging

De ingebouwde servicecontainer is ontworpen voor de behoeften van het framework en de meeste consumenten-apps. U wordt aangeraden de ingebouwde container te gebruiken, tenzij u een specifieke functie nodig hebt die niet wordt ondersteund, zoals:

  • Eigenschapsinjectie
  • Injectie op basis van naam (alleen.NET 7 en eerdere versies). Zie Keyed-services voor meer informatie.)
  • Onderliggende containers
  • Aangepast levensduurbeheer
  • Func<T> ondersteuning voor luie initialisatie
  • Registratie op basis van conventies

De volgende containers van derden kunnen worden gebruikt met ASP.NET Core-apps:

Schroefdraadveiligheid

Thread-safe singleton-services maken. Als een singleton-service afhankelijk is van een tijdelijke service, kan de tijdelijke service ook threadveiligheid vereisen, afhankelijk van hoe deze wordt gebruikt door de singleton.

De factorymethode van een singleton-service, zoals het tweede argument voor AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), hoeft niet thread-safe te zijn. Net als bij een typeconstructorstatic wordt gegarandeerd slechts één keer door één thread aangeroepen.

Aanbevelingen

  • async/await en Task op basis van serviceomzetting wordt niet ondersteund. Omdat C# geen asynchrone constructors ondersteunt, gebruikt u asynchrone methoden nadat de service synchroon is opgelost.
  • Vermijd het opslaan van gegevens en configuratie rechtstreeks in de servicecontainer. Het winkelwagentje van een gebruiker mag bijvoorbeeld meestal niet worden toegevoegd aan de servicecontainer. Voor de configuratie moet het optiespatroon worden gebruikt. Vermijd op dezelfde manier 'gegevenshouder'-objecten die alleen bestaan om toegang tot een ander object toe te staan. Het is beter om het werkelijke item via DI aan te vragen.
  • Voorkom statische toegang tot services. Vermijd bijvoorbeeld het vastleggen van IApplicationBuilder.ApplicationServices als een statisch veld of een eigenschap voor gebruik elders.
  • Houd DI-factory's snel en synchroon.
  • Vermijd het gebruik van het servicezoekerpatroon. Roep bijvoorbeeld niet GetService aan om een service-exemplaar te verkrijgen wanneer u in plaats daarvan DI kunt gebruiken.
  • Een andere servicezoekervariatie om te voorkomen, is het injecteren van een factory die afhankelijkheden tijdens runtime oplost. Beide werkwijzen combineren Inversion of Control-strategieën .
  • Vermijd aanroepen bij BuildServiceProvider het configureren van services. Het aanroepen BuildServiceProvider gebeurt meestal wanneer de ontwikkelaar een service wil oplossen bij het registreren van een andere service. Gebruik in plaats daarvan een overbelasting die de om deze reden omvat IServiceProvider .
  • Tijdelijke wegwerpdiensten worden vastgelegd door de container voor verwijdering. Dit kan worden omgezet in een geheugenlek als deze is opgelost vanuit de container op het hoogste niveau.
  • Schakel bereikvalidatie in om ervoor te zorgen dat de app geen singletons heeft die scoped services vastleggen. Zie Bereikvalidatie voor meer informatie.

Net als bij alle sets aanbevelingen kunnen situaties optreden waarin het negeren van een aanbeveling vereist is. Uitzonderingen zijn zeldzaam, meestal speciale gevallen binnen het framework zelf.

DI is een alternatief voor statische/globale objecttoegangspatronen. Mogelijk kunt u de voordelen van DI niet realiseren als u deze combineert met statische objecttoegang.

Voorbeeld van antipatronen

Naast de richtlijnen in dit artikel zijn er verschillende antipatronen die u moet vermijden. Sommige van deze antipatronen leren van het ontwikkelen van de runtimes zelf.

Waarschuwing

Dit zijn voorbeelden van antipatronen, kopieer de code niet , gebruik deze patronen niet en vermijd deze patronen in alle kosten.

Tijdelijke wegwerpservices vastgelegd door container

Wanneer u tijdelijke services registreert die worden geïmplementeerdIDisposable, houdt de DI-container standaard deze verwijzingen vast en niet Dispose() totdat de container wordt verwijderd wanneer de toepassing stopt als ze zijn omgezet vanuit de container, of totdat het bereik wordt verwijderd als ze zijn opgelost vanuit een bereik. Dit kan worden omgezet in een geheugenlek als deze is opgelost vanaf containerniveau.

Anti-pattern: Transient disposables without dispose. Do not copy!

In het voorgaande antipatroon worden 1000 ExampleDisposable objecten geïnstantieerd en geroot. Ze worden pas verwijderd nadat het serviceProvider exemplaar is verwijderd.

Zie Fouten opsporen in een geheugenlek in .NET voor meer informatie over het opsporen van fouten in geheugenlekken.

Asynchrone DI-factory's kunnen impasses veroorzaken

De term 'DI factory's' verwijst naar de overbelastingsmethoden die bestaan bij het aanroepen Add{LIFETIME}. Er zijn overbelastingen die een Func<IServiceProvider, T> locatie accepteren waar T de service wordt geregistreerd en de parameter de naam implementationFactoryheeft. De implementationFactory kan worden opgegeven als een lambda-expressie, lokale functie of methode. Als de fabriek asynchroon is en u gebruikt Task<TResult>.Result, veroorzaakt dit een impasse.

Anti-pattern: Deadlock with async factory. Do not copy!

In de voorgaande code krijgt u implementationFactory een lambda-expressie waarin de hoofdtekst een Task<Bar> retourmethode aanroeptTask<TResult>.Result. Dit veroorzaakt een impasse. De GetBarAsync methode emuleert gewoon een asynchrone werkbewerking met Task.Delayen roept vervolgens aan GetRequiredService<T>(IServiceProvider).

Anti-pattern: Deadlock with async factory inner issue. Do not copy!

Zie Asynchrone programmering voor meer informatie over asynchrone richtlijnen : Belangrijke informatie en advies. Zie Fouten opsporen in een impasse in .NET voor meer informatie over het opsporen van impasses.

Wanneer u dit antipatroon uitvoert en de impasse optreedt, kunt u de twee threads bekijken die wachten vanuit het venster Parallel Stacks van Visual Studio. Zie Threads en taken weergeven in het venster Parallelle stacks voor meer informatie.

Captive-afhankelijkheid

De term 'captive dependency' werd bedacht door Mark Seemann en verwijst naar de onjuiste configuratie van de levensduur van de service, waarbij een langerlevende service een kortere service-captive bevat.

Anti-pattern: Captive dependency. Do not copy!

In de voorgaande code Foo wordt deze geregistreerd als een singleton en Bar is het bereik - dat op het oppervlak geldig lijkt. Houd echter rekening met de tenuitvoerlegging van Foo.

namespace DependencyInjection.AntiPatterns;

public class Foo(Bar bar)
{
}

Voor het Foo object is een Bar object vereist en omdat Foo het een singleton is en Bar het bereik heeft, is dit een onjuiste configuratie. Zoals wel, Foo zou slechts één keer worden geïnstantieerd, en het zou gedurende zijn levensduur blijven, Bar wat langer is dan de beoogde levensduur van Bar. U moet overwegen om bereiken te valideren door validateScopes: true naar de BuildServiceProvider(IServiceCollection, Boolean). Wanneer u de bereiken valideert, krijgt u een InvalidOperationException bericht met een bericht dat lijkt op 'Kan de scoped service 'Bar' niet gebruiken van singleton 'Foo'.'

Zie Bereikvalidatie voor meer informatie.

Scoped-service als singleton

Wanneer u scoped services gebruikt, wordt de service een singleton als u geen bereik of binnen een bestaand bereik maakt.

Anti-pattern: Scoped service becomes singleton. Do not copy!

In de voorgaande code wordt Bar deze opgehaald binnen een IServiceScope, wat juist is. Het antipatroon is het ophalen van Bar buiten het bereik en de variabele krijgt de naam avoid om aan te geven welk voorbeeld ophalen onjuist is.

Zie ook