Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
.NET unterstützt das Di-Softwaredesignmuster ( Dependency Injection ), das eine Technik zum Erreichen von Inversion of Control (IoC) zwischen Klassen und deren Abhängigkeiten ist. Neben der Konfiguration, der Protokollierung und dem Optionsmuster ist die Abhängigkeitsinjektion in .NET ein integrierter Bestandteil des Frameworks.
Eine Abhängigkeit ist ein Objekt, von dem ein anderes Objekt abhängig ist. Die folgende MessageWriter Klasse verfügt über eine Write Methode, von der andere Klassen abhängig sein können:
public class MessageWriter : IMessageWriter
{
public void Write(string message)
{
Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
}
}
Eine Klasse kann eine Instanz der MessageWriter Klasse erstellen, um ihre Write Methode zu verwenden. Im folgenden Beispiel ist die MessageWriter Klasse eine Abhängigkeit der 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 diesem Fall erstellt und hängt die Worker Klasse direkt von der MessageWriter Klasse ab. Hartcodierte Abhängigkeiten wie dies sind problematisch und sollten aus folgenden Gründen vermieden werden:
- Um durch eine andere Implementierung zu ersetzen
MessageWriter, müssen Sie dieWorkerKlasse ändern. - Wenn
MessageWriterAbhängigkeiten vorhanden sind, muss dieWorkerKlasse sie auch konfigurieren. In einem großen Projekt mit mehreren Klassen, die vonMessageWriterabhängig sind, wird der Konfigurationscode über die App verteilt. - Diese Implementierung ist nicht für Komponententests geeignet. Die App sollte eine
MessageWriter-Modell- oder Stubklasse verwenden, was mit diesem Ansatz nicht möglich ist.
Das Konzept
Die Abhängigkeitsinjektion behebt hartcodierte Abhängigkeitsprobleme durch:
Die Verwendung einer Schnittstelle oder Basisklasse zur Abstraktion der Abhängigkeitsimplementierung.
Registrierung der Abhängigkeit in einem Dienstcontainer.
.NET stellt einen integrierten Dienstcontainer (IServiceProvider) bereit. Dienste werden in der Regel beim Start der App registriert und einer IServiceCollection angefügt. Nachdem alle Dienste hinzugefügt wurden, verwenden Sie BuildServiceProvider, um den Dienstcontainer zu erstellen.
Einfügen des Diensts in den Konstruktor der Klasse, in der er verwendet wird.
Das Framework erstellt eine Instanz der Abhängigkeit und entfernt diese, wenn sie nicht mehr benötigt wird.
Tipp
Bei der Terminologie zum Einfügen von Abhängigkeiten ist ein Dienst in der Regel ein Objekt, das einen Dienst für andere Objekte bereitstellt, z. B. den IMessageWriter Dienst. Der Dienst ist nicht mit einem Webdienst verknüpft, obwohl er möglicherweise einen Webdienst verwendet.
Nehmen wir beispielsweise an, dass die Schnittstelle die IMessageWriterWrite Methode definiert. Diese Schnittstelle wird durch einen konkreten Typ implementiert, MessageWriterder zuvor gezeigt wird. Im folgenden Beispielcode wird der IMessageWriter Dienst mit dem konkreten Typ MessageWriterregistriert. Die AddSingleton Methode registriert den Dienst mit einer Singleton-Lebensdauer, was bedeutet, dass er erst verworfen wird, wenn die App heruntergefahren wird.
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>
Im vorherigen Codebeispiel sind die hervorgehobenen Zeilen:
- Erstellen Sie eine Host-App-Generator-Instanz.
- Konfigurieren Sie die Dienste, indem Sie den
Workerals gehosteten Dienst und dieIMessageWriterSchnittstelle als Singletondienst mit einer entsprechenden Implementierung derMessageWriterKlasse registrieren. - Erstellen Sie den Host, und führen Sie ihn aus.
Der Host enthält den Abhängigkeitsinjektion-Dienstanbieter. Er enthält auch alle anderen relevanten Dienste zur automatischen Worker-Instanziierung und zum Bereitstellen der entsprechenden IMessageWriter-Implementierung als Argument.
Bei Verwendung des DI-Musters verwendet der Workerdienst nicht den konkreten Typ MessageWriter, nur die IMessageWriter Schnittstelle, die es implementiert. Dieses Design erleichtert das Ändern der Implementierung, die der Worker-Service verwendet, ohne den Worker-Service selbst zu modifizieren. Der Worker-Dienst erstellt auch keine Instanz von MessageWriter. Der DI-Container erstellt die Instanz.
Stellen Sie sich nun vor, Sie möchten MessageWriter durch einen Typ ersetzen, der den vom Framework bereitgestellten Protokollierungsdienst verwendet. Erstellen Sie eine Klasse LoggingMessageWriter, die von ILogger<TCategoryName> abhängt, indem sie im Konstruktor angefordert wird.
public class LoggingMessageWriter(
ILogger<LoggingMessageWriter> logger) : IMessageWriter
{
public void Write(string message) =>
logger.LogInformation("Info: {Msg}", message);
}
Um von MessageWriter zu LoggingMessageWriter zu wechseln, aktualisieren Sie einfach den Aufruf, um diese neue AddSingleton-Implementierung zu registrieren:
builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();
Tipp
Der Container löst ILogger<TCategoryName> auf, indem er offene (generische) Typen nutzt, wodurch die Notwendigkeit entfällt, jeden (generischen) konstruierten Typ zu registrieren.
Verhalten von Constructor Injection
Dienste können mit IServiceProvider (dem integrierten Dienstcontainer) oder ActivatorUtilities.
ActivatorUtilities erstellt Objekte, die nicht im Container registriert sind und mit einigen Frameworkfeatures verwendet werden.
Konstruktoren können Argumente akzeptieren, die nicht durch Abhängigkeitsinjektion bereitgestellt werden. Die Argumente müssen jedoch Standardwerte zuweisen.
Wenn IServiceProvider oder ActivatorUtilities Dienste auflöst, erfordert die Konstruktoreinjektion einen öffentlichen Konstruktor.
Wenn ActivatorUtilities Dienste auflöst, erfordert die Konstruktorinjektion, dass nur ein anwendbarer Konstruktor existiert. Konstruktorüberladungen werden unterstützt. Es darf jedoch nur eine Überladung vorhanden sein, deren Argumente alle durch Dependency Injection erfüllt werden können.
Konstruktorauswahlregeln
Wenn ein Typ mehrere Konstruktoren definiert, verfügt der Dienstanbieter über eine Logik zum Ermitteln des zu verwendenden Konstruktors. Der Konstruktor mit den meisten Parametern, deren Typen per Abhängigkeitsinjektion aufgelöst werden können, wird ausgewählt. Betrachten Sie den folgenden Beispieldienst:
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(ILogger<ExampleService> logger)
{
// ...
}
public ExampleService(ServiceA serviceA, ServiceB serviceB)
{
// ...
}
}
Gehen Sie im vorherigen Code davon aus, dass das Logging hinzugefügt wurde und vom Dienstanbieter auflösbar ist, aber die Typen ServiceA und ServiceB sind es nicht. Der Konstruktor mit dem ILogger<ExampleService> Parameter löst die ExampleService Instanz auf. Obwohl es einen Konstruktor gibt, der weitere Parameter definiert, sind die Typen ServiceA und ServiceB nicht über Dependency Injection (DI) auflösbar.
Wenn beim Ermitteln von Konstruktoren Mehrdeutigkeiten bestehen, wird eine Ausnahme ausgelöst. Sehen Sie sich den folgenden C#-Beispieldienst an:
Warnung
Dieser ExampleService Code mit uneindeutigen, DI-entschlüsselbaren Typparametern löst eine Ausnahme aus.
Tu das nicht – es soll zeigen, was mit "mehrdeutig auflösbaren DI-Typen" gemeint ist.
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(ILogger<ExampleService> logger)
{
// ...
}
public ExampleService(IOptions<ExampleOptions> options)
{
// ...
}
}
Im vorherigen Beispiel gibt es drei Konstruktoren. Der erste Konstruktor ist parameterlos und erfordert keine Dienste vom Dienstanbieter. Angenommen, dem Abhängigkeitsinjektionscontainer wurden die Protokollierung und Optionen hinzugefügt und diese Dienste können per Abhängigkeitsinjektion aufgelöst werden. Wenn der DI-Container versucht, den ExampleService Typ aufzulösen, löst er eine Ausnahme aus, da die beiden Konstruktoren mehrdeutig sind.
Vermeiden Sie Mehrdeutigkeit, indem Sie einen Konstruktor definieren, der stattdessen beide DI-Auflösungstypen akzeptiert:
public class ExampleService
{
public ExampleService()
{
}
public ExampleService(
ILogger<ExampleService> logger,
IOptions<ExampleOptions> options)
{
// ...
}
}
Bereichsvalidierung
Bereichsbezogene Dienste werden vom Container entsorgt, der sie erstellt hat. Wenn ein bereichsbezogener Dienst im Stammcontainer erstellt wird, wird die Lebensdauer des Diensts effektiv auf Singleton heraufgestuft, da er nur vom Stammcontainer entfernt wird, wenn die App heruntergefahren wird. Die Überprüfung bereichsbezogener Dienste erfasst diese Situationen, wenn BuildServiceProvider aufgerufen wird.
Wenn eine App in der Entwicklungsumgebung ausgeführt wird und CreateApplicationBuilder aufruft , um den Host zu erstellen, führt der Standarddienstanbieter Überprüfungen durch, um folgendes zu überprüfen:
- bereichsbezogene Dienste nicht vom Stammdienstanbieter aufgelöst werden.
- bereichsbezogene Dienste nicht in Singletons eingefügt werden.
Bereichsszenarios
IServiceScopeFactory ist immer als Singleton registriert, aber IServiceProvider kann je nach Lebensdauer der enthaltenden Klasse variieren. Wenn Sie beispielsweise Dienste aus einem Bereich auflösen und einer dieser Dienste eine IServiceProvider annimmt, dann ist es eine bereichsbezogene Instanz.
Um Bereichsdefinitionsdienste innerhalb von Implementierungen von IHostedService, z. B. der BackgroundService, zu erreichen, fügen Sie die Dienstabhängigkeiten nicht über die Konstruktoreinfügung ein. Fügen Sie stattdessen IServiceScopeFactory ein, erstellen Sie einen Bereich, und lösen Sie dann Abhängigkeiten aus dem Bereich auf, um die entsprechende Dienstlebensdauer zu verwenden.
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);
}
}
}
}
}
Im vorangehenden Code trifft Folgendes auf den Hintergrunddienst zu, während die App ausgeführt wird:
- Er hängt von IServiceScopeFactory ab.
- Erstellt einen IServiceScope zum Auflösen anderer Dienste.
- Er löst bereichsbezogene Dienste für den Verbrauch auf.
- Er verarbeitet Objekte, leitet sie dann weiter und markiert sie schließlich als verarbeitet.
Im Beispielquellcode können Sie sehen, wie Implementierungen von IHostedService von bereichsbezogenen Dienstlebensdauern profitieren können.
Schlüsseldienste
Sie können Dienste registrieren und Nachschlagevorgänge basierend auf einem Schlüssel ausführen. Mit anderen Worten: Es ist möglich, mehrere Dienste mit unterschiedlichen Schlüsseln zu registrieren und diesen Schlüssel für die Suche zu verwenden.
Betrachten Sie z. B. den Fall, in dem Sie unterschiedliche Implementierungen der Schnittstelle IMessageWriterhaben: MemoryMessageWriter und QueueMessageWriter.
Sie können diese Dienste mithilfe der Überladung der Dienstregistrierungsmethoden (siehe weiter oben) registrieren, die einen Schlüssel als Parameter unterstützen:
services.AddKeyedSingleton<IMessageWriter, MemoryMessageWriter>("memory");
services.AddKeyedSingleton<IMessageWriter, QueueMessageWriter>("queue");
Das key ist nicht auf string. Das key kann jedes beliebige object sein, solange der Typ die Schnittstelle Equals korrekt implementiert.
Im Konstruktor der Klasse, die IMessageWriter verwendet, fügen Sie die FromKeyedServicesAttribute hinzu, um den Schlüssel des aufzulösenden Diensts anzugeben:
public class ExampleService
{
public ExampleService(
[FromKeyedServices("queue")] IMessageWriter writer)
{
// Omitted for brevity...
}
}
KeyedService.AnyKey-Eigenschaft
Die KeyedService.AnyKey Eigenschaft stellt einen speziellen Schlüssel für das Arbeiten mit Schlüsseldiensten bereit. Sie können einen Dienst als Fallback mit KeyedService.AnyKey registrieren, der zu jedem Schlüssel passt. Dies ist nützlich, wenn Sie eine Standardimplementierung für jeden Schlüssel bereitstellen möchten, der nicht über eine explizite Registrierung verfügt.
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}");
Im vorherigen Beispiel:
- Das Anfordern
ICachemit Schlüssel"premium"gibt diePremiumCacheInstanz zurück. - Beim Anfordern von
ICachemit jedem anderen Schlüssel (z. B."basic"oder"standard") wird mithilfe desDefaultCache-Fallbacks ein neuesAnyKeyerstellt.
Von Bedeutung
Ab .NET 10 löst der Aufruf von GetKeyedService() mit KeyedService.AnyKey eine InvalidOperationException aus, da AnyKey als Registrierungsfallback gedacht ist und nicht als Abfrageschlüssel. Weitere Informationen finden Sie unter Beheben von Problemen in GetKeyedService() und GetKeyedServices() mit AnyKey.
Siehe auch
- Schnellstart: Grundlagen der Abhängigkeitsinjektion
- Tutorial: Verwenden der Abhängigkeitsinjektion in .NET
- Richtlinien für die Dependency Injection
- Abhängigkeitsinjektion in ASP.NET Core
- NDC-Konferenzmuster für die DI-App-Entwicklung
- Prinzip der expliziten Abhängigkeiten
- Umkehrung von Steuerungscontainern und das Abhängigkeitsinjektionsmuster (Martin Fowler)