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:
- Ontvang een IServiceProvider in de constructor.
- Gebruik ActivatorUtilities.CreateInstance dit om het exemplaar buiten de container te instantiëren, terwijl u de container gebruikt voor de bijbehorende afhankelijkheden.
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
enTask
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 omvatIServiceProvider
. - 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.
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 implementationFactory
heeft. 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.
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).
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.
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.
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.