Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
.NET ondersteunt het ontwerppatroon voor afhankelijkheidsinjectie (DI), een techniek voor het bereiken van Inversion of Control (IoC) tussen klassen en hun afhankelijkheden. Afhankelijkheidsinjectie in .NET is een ingebouwd onderdeel van het framework, samen met configuratie, logboekregistratie en het optiespatroon.
Een afhankelijkheid is een object waarop een ander object afhankelijk is. De volgende MessageWriter klasse heeft een Write methode die afhankelijk kan zijn van andere klassen:
public class MessageWriter : IMessageWriter
{
public void Write(string message)
{
Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
}
}
Een klasse kan een exemplaar van de MessageWriter klasse maken om de Write bijbehorende methode te gebruiken. In het volgende voorbeeld is de MessageWriter klasse een afhankelijkheid van de Worker klasse:
public class Worker : BackgroundService
{
private readonly MessageWriter _messageWriter = new();
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
_messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
await Task.Delay(1_000, stoppingToken);
}
}
}
In dit geval is de Worker klasse gemaakt en rechtstreeks afhankelijk van de MessageWriter klasse. In code vastgelegde afhankelijkheden zoals deze zijn problematisch en moeten om de volgende redenen worden vermeden:
- Om
MessageWriterte vervangen door een andere implementatie, moet u deWorkerclass wijzigen. - Als er afhankelijkheden zijn in
MessageWriter, moet deWorkerklasse deze ook configureren. In een groot project met meerdere klassen, afhankelijk vanMessageWriter, wordt de configuratiecode verspreid over de app. - Deze implementatie is moeilijk te testen. De app moet een mock- of stub-
MessageWriter-klasse gebruiken, wat niet mogelijk is met deze benadering.
Het concept
Afhankelijkheidsinjectie lost problemen met in code vastgelegde afhankelijkheid op via:
Het gebruik van een interface of basisklasse om de implementatie van afhankelijkheden te abstraheren.
Registratie van de afhankelijkheid in een servicecontainer.
.NET biedt een ingebouwde servicecontainer. IServiceProvider Services worden doorgaans geregistreerd bij het opstarten van de app en toegevoegd aan een IServiceCollection. Zodra alle services zijn toegevoegd, gebruik BuildServiceProvider om de servicecontainer te maken.
Injectie van de service in de constructor van de klasse waar deze wordt gebruikt.
Het framework neemt de verantwoordelijkheid om een instantie van de afhankelijkheid te creëren en het af te breken wanneer dat niet meer nodig is.
Aanbeveling
In terminologie voor afhankelijkheidsinjectie is een service doorgaans een object dat een service aan andere objecten biedt, zoals de IMessageWriter service. De service is niet gerelateerd aan een webservice, hoewel deze mogelijk een webservice gebruikt.
Stel dat de IMessageWriter interface de Write-methode definieert. Deze interface wordt geïmplementeerd door een concreet type, MessageWriter, dat eerder werd weergegeven. Met de volgende voorbeeldcode wordt de IMessageWriter service geregistreerd bij het betontype MessageWriter. De AddSingleton methode registreert de service met een singleton-levensduur, wat betekent dat deze pas wordt verwijderd nadat de app is afgesloten.
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
builder.Services.AddSingleton<IMessageWriter, MessageWriter>();
using IHost host = builder.Build();
host.Run();
// <SnippetMW>
public class MessageWriter : IMessageWriter
{
public void Write(string message)
{
Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
}
}
// </SnippetMW>
// <SnippetIMW>
public interface IMessageWriter
{
void Write(string message);
}
// </SnippetIMW>
// <SnippetWorker>
public sealed class Worker(IMessageWriter messageWriter) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
await Task.Delay(1_000, stoppingToken);
}
}
}
// </SnippetWorker>
In het voorgaande codevoorbeeld zijn de gemarkeerde regels:
- Maak een instantie van een hostapp-bouwer.
- Configureer de services door de
Workerservice te registreren als een gehoste service en deIMessageWriterinterface als een singleton-service met een bijbehorende implementatie van deMessageWriterklasse. - Bouw de host en voer deze uit.
De host bevat de provider van de afhankelijkheidsinjectieservice. Het bevat ook alle andere relevante services die nodig zijn om de Worker implementatie automatisch te instantiëren en de bijbehorende IMessageWriter implementatie als argument te verstrekken.
Door het DI-patroon te gebruiken, maakt de worker service geen gebruik van het concrete type MessageWriter, alleen van de IMessageWriter interface die het implementeert. Met dit ontwerp kunt u eenvoudig de implementatie wijzigen die de worker service gebruikt, zonder de worker service zelf te wijzigen. De service voor workers maakt ook geen exemplaar van MessageWriter. De DI-container maakt de instantie.
Stel dat u MessageWriter wilt vervangen door een type dat gebruikmaakt van de door het framework geleverde logservice. Maak een klasse LoggingMessageWriter die afhankelijk ILogger<TCategoryName> is van door deze aan te vragen in de constructor.
public class LoggingMessageWriter(
ILogger<LoggingMessageWriter> logger) : IMessageWriter
{
public void Write(string message) =>
logger.LogInformation("Info: {Msg}", message);
}
Om over te schakelen van MessageWriter naar LoggingMessageWriter, werkt u de aanroep naar AddSingleton bij om deze nieuwe IMessageWriter-implementatie te registreren.
builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();
Aanbeveling
De container lost ILogger<TCategoryName> op door gebruik te maken van (generieke) open typen, waardoor het niet meer nodig is om elk (generiek) geconstrueerd type te registreren.
Gedrag van constructorinjectie
Services kunnen worden opgelost met behulp van IServiceProvider (de ingebouwde servicecontainer) of ActivatorUtilities.
ActivatorUtilities maakt objecten die niet zijn geregistreerd in de container en wordt gebruikt met sommige frameworkfuncties.
Constructors kunnen argumenten accepteren die niet worden geleverd door afhankelijkheidsinjectie, maar de argumenten moeten standaardwaarden toewijzen.
Als IServiceProvider of ActivatorUtilities services oplossen, vereist constructorinjectie een openbare constructor.
Wanneer ActivatorUtilities services worden opgelost, vereist constructorinjectie dat er slechts één toepasselijke constructor bestaat. Overbelasting van constructors wordt ondersteund, maar er kan slechts één overbelasting bestaan waarvan alle argumenten kunnen worden vervuld door afhankelijkheidsinjectie.
Selectieregels voor de constructor
Wanneer een type meer dan één constructor definieert, heeft de serviceprovider logica om te bepalen welke constructor moet worden gebruikt. De constructor met de meeste parameters waarin de typen DI-omzetbaar zijn, is geselecteerd. Bekijk de volgende voorbeeldservice:
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(ILogger<ExampleService> logger)
{
// ...
}
public ExampleService(ServiceA serviceA, ServiceB serviceB)
{
// ...
}
}
In de voorgaande code wordt aangenomen dat logboekregistratie is toegevoegd en beschikbaar is via de serviceprovider, maar dat de typen ServiceA en ServiceB niet beschikbaar zijn. De constructor met de ILogger<ExampleService> parameter lost het ExampleService exemplaar op. Hoewel er een constructor is die meer parameters definieert, zijn de ServiceA en ServiceB typen niet DI-omzetbaar.
Als er dubbelzinnigheid is bij het detecteren van constructors, wordt er een uitzondering gegenereerd. Bekijk de volgende C#-voorbeeldservice:
Waarschuwing
Deze ExampleService code met dubbelzinnige DI-omzetbare typeparameters genereert een uitzondering.
Doe dit niet , het is bedoeld om te laten zien wat wordt bedoeld door 'dubbelzinnige DI-oplossingstypen'.
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(ILogger<ExampleService> logger)
{
// ...
}
public ExampleService(IOptions<ExampleOptions> options)
{
// ...
}
}
In het voorgaande voorbeeld zijn er drie constructors. De eerste constructor is parameterloos en vereist geen services van de serviceprovider. Stel dat zowel logboekregistratie als opties zijn toegevoegd aan de DI-container en dat dit di-omzetbare services zijn. Wanneer de DI-container probeert het ExampleService type op te lossen, genereert deze een uitzondering, omdat de twee constructors dubbelzinnig zijn.
Vermijd dubbelzinnigheid door een constructor te definiëren die in plaats daarvan beide DI-oplossingstypen accepteert:
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(
ILogger<ExampleService> logger,
IOptions<ExampleOptions> options)
{
// ...
}
}
Bereikvalidatie
Scoped services worden verwijderd door de container die ze heeft gecreëerd. Als een scoped service wordt gemaakt in de hoofdcontainer, wordt de levensduur van de service effectief gepromoveerd tot singleton , omdat deze alleen wordt verwijderd door de hoofdcontainer wanneer de app wordt afgesloten. Bij het valideren van servicebereiken worden deze situaties onderschept wanneer BuildServiceProvider wordt aangeroepen.
Wanneer een app wordt uitgevoerd in de ontwikkelomgeving en CreateApplicationBuilder aanroept om de host te bouwen, voert de standaardserviceprovider controles uit om te controleren of:
- Scoped services worden niet omgezet vanuit de hoofdserviceprovider.
- Scoped-services worden niet in singletons geïnjecteerd.
Bereikscenario's
De IServiceScopeFactory is altijd geregistreerd als een singleton, maar de IServiceProvider kan variëren op basis van de levensduur van de betreffende klasse. Als u bijvoorbeeld services van een bereik oplost en een van deze services een IServiceProviderbereik neemt, is dit een instantie binnen het bereik.
Om gebruik te maken van scoped services binnen implementaties van IHostedService, zoals BackgroundService, moet u de serviceafhankelijkheden niet via constructor injectie injecteren. Injecteer IServiceScopeFactoryin plaats daarvan, maak een bereik en los vervolgens afhankelijkheden van het bereik op om de juiste levensduur van de service te gebruiken.
namespace WorkerScope.Example;
public sealed class Worker(
ILogger<Worker> logger,
IServiceScopeFactory serviceScopeFactory)
: BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
using (IServiceScope scope = serviceScopeFactory.CreateScope())
{
try
{
logger.LogInformation(
"Starting scoped work, provider hash: {hash}.",
scope.ServiceProvider.GetHashCode());
var store = scope.ServiceProvider.GetRequiredService<IObjectStore>();
var next = await store.GetNextAsync();
logger.LogInformation("{next}", next);
var processor = scope.ServiceProvider.GetRequiredService<IObjectProcessor>();
await processor.ProcessAsync(next);
logger.LogInformation("Processing {name}.", next.Name);
var relay = scope.ServiceProvider.GetRequiredService<IObjectRelay>();
await relay.RelayAsync(next);
logger.LogInformation("Processed results have been relayed.");
var marked = await store.MarkAsync(next);
logger.LogInformation("Marked as processed: {next}", marked);
}
finally
{
logger.LogInformation(
"Finished scoped work, provider hash: {hash}.{nl}",
scope.ServiceProvider.GetHashCode(), Environment.NewLine);
}
}
}
}
}
In de voorgaande code, terwijl de app wordt uitgevoerd, wordt de achtergrondservice uitgevoerd:
- Afhankelijk van de IServiceScopeFactory.
- Hiermee maakt u een IServiceScope voor het oplossen van andere services.
- Hiermee worden scoped services voor verbruik omgezet.
- Werkt aan het verwerken van objecten en geeft ze vervolgens door en markeert ze ten slotte als verwerkt.
In de voorbeeldbroncode kunt u zien hoe implementaties van IHostedService de scoped servicelevensduur kunnen profiteren.
Sleutelservices
U kunt services registreren en zoekacties uitvoeren op basis van een sleutel. Met andere woorden, het is mogelijk om meerdere services met verschillende sleutels te registreren en deze sleutel te gebruiken voor de zoekactie.
Denk bijvoorbeeld aan het geval waarin u verschillende implementaties van de interface IMessageWriterhebt: MemoryMessageWriter en QueueMessageWriter.
U kunt deze services registreren met behulp van de overbelasting van de serviceregistratiemethoden (eerder gezien) die ondersteuning bieden voor een sleutel als parameter:
services.AddKeyedSingleton<IMessageWriter, MemoryMessageWriter>("memory");
services.AddKeyedSingleton<IMessageWriter, QueueMessageWriter>("queue");
De key is niet beperkt tot string. De key kan elk object zijn dat u wilt, zolang het type correct Equals implementeert.
In de constructor van de klasse die gebruikmaakt IMessageWriter, voegt u de FromKeyedServicesAttribute klasse toe om de sleutel van de service op te geven die u wilt oplossen:
public class ExampleService
{
public ExampleService(
[FromKeyedServices("queue")] IMessageWriter writer)
{
// Omitted for brevity...
}
}
KeyedService.AnyKey-eigenschap
De KeyedService.AnyKey eigenschap biedt een speciale sleutel voor het werken met services met sleutels. U kunt een service registreren met behulp van KeyedService.AnyKey als een terugvalmogelijkheid die overeenkomt met elke sleutel. Dit is handig als u een standaard implementatie wilt opgeven voor een sleutel die geen expliciete registratie heeft.
var services = new ServiceCollection();
// Register a fallback cache for any key.
services.AddKeyedSingleton<ICache>(KeyedService.AnyKey, (sp, key) =>
{
// Create a cache instance based on the key.
return new DefaultCache(key?.ToString() ?? "unknown");
});
// Register a specific cache for the "premium" key.
services.AddKeyedSingleton<ICache>("premium", new PremiumCache());
var provider = services.BuildServiceProvider();
// Requesting with "premium" key returns PremiumCache.
var premiumCache = provider.GetKeyedService<ICache>("premium");
Console.WriteLine($"Premium key: {premiumCache}");
// Requesting with any other key uses the AnyKey fallback.
var basicCache = provider.GetKeyedService<ICache>("basic");
Console.WriteLine($"Basic key: {basicCache}");
var standardCache = provider.GetKeyedService<ICache>("standard");
Console.WriteLine($"Standard key: {standardCache}");
In het voorgaande voorbeeld:
- Wanneer
ICachewordt aangevraagd met sleutel"premium", retourneert dit dePremiumCacheinstantie. - Wanneer u
ICacheaanvraagt met een andere sleutel (zoals"basic"of"standard"), wordt er een nieuweDefaultCachegemaakt met behulp van deAnyKeyterugval.
Belangrijk
Vanaf .NET 10 genereert het aanroepen GetKeyedService() met KeyedService.AnyKey een InvalidOperationException omdat AnyKey is bedoeld als een terugval voor registratie, niet als een querysleutel. Zie Problemen oplossen in GetKeyedService() en GetKeyedServices() met AnyKey voor meer informatie.
Zie ook
- Quickstart: Basisprincipes van afhankelijkheidsinjectie
- Zelfstudie: Afhankelijkheidsinjectie gebruiken in .NET
- Richtlijnen voor Afhankelijkheidsinjectie
- Afhankelijkheidsinjectie in ASP.NET Core
- NDC Conferentiepatronen voor DI-appontwikkeling
- Expliciete afhankelijkhedenprincipe
- Inversie van controlecontainers en het patroon afhankelijkheidsinjectie (Martin Fowler)