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.
Dieser Artikel enthält allgemeine Richtlinien und bewährte Methoden für die Implementierung von Abhängigkeitseinfügungen (DI) in .NET-Anwendungen.
Entwerfen von Diensten für die Abhängigkeitsinjektion
Beachten Sie Folgendes beim Entwerfen von Diensten für Dependency Injection:
- Vermeiden Sie zustandsbehaftete statische Klassen und Member. Vermeiden Sie das Erstellen eines globalen Zustands, indem Sie Apps stattdessen zur Verwendung von Singletondiensten entwerfen.
- Vermeiden Sie die direkte Instanziierung abhängiger Klassen innerhalb von Diensten. Die direkte Instanziierung koppelt den Code an eine bestimmte Implementierung.
- Erstellen Sie kleine, gut gestaltete und einfach zu testende Dienste.
Wenn eine Klasse viele Abhängigkeitsinjektionen aufweist, ist dies möglicherweise ein Anzeichen dafür, dass die Klasse zu viele Aufgaben umfasst und gegen das Prinzip der einzigen Verantwortung (SRP, Single Responsibility Principle) verstößt. Versuchen Sie, die Klasse umzugestalten, indem Sie einige ihrer Verantwortung in neue Klassen verschieben.
Löschen von Diensten
Der Container ist für die Bereinigung der von ihm erstellten Typen zuständig und ruft Dispose für IDisposable-Instanzen auf. Dienste, die aus dem Container aufgelöst werden, sollten nie vom Entwickler gelöscht werden. Wenn ein Typ oder eine Factory als Singleton registriert ist, wird das Singleton automatisch vom Container verworfen.
Im folgenden Beispiel werden die Dienste vom Dienstcontainer erstellt und automatisch verworfen:
namespace ConsoleDisposable.Example;
public sealed class TransientDisposable : IDisposable
{
public void Dispose() => Console.WriteLine($"{nameof(TransientDisposable)}.Dispose()");
}
Für das vorhergehende verwerfbare Objekt ist eine vorübergehende Lebensdauer vorgesehen.
namespace ConsoleDisposable.Example;
public sealed class ScopedDisposable : IDisposable
{
public void Dispose() => Console.WriteLine($"{nameof(ScopedDisposable)}.Dispose()");
}
Für das vorhergehende verwerfbare Objekt ist eine bereichsbezogene Lebensdauer vorgesehen.
namespace ConsoleDisposable.Example;
public sealed class SingletonDisposable : IDisposable
{
public void Dispose() => Console.WriteLine($"{nameof(SingletonDisposable)}.Dispose()");
}
Für das vorhergehende verwerfbare Objekt ist eine Singletonlebensdauer vorgesehen.
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>();
}
Die Debugging-Konsole zeigt nach der Ausführung die folgende Beispielausgabe an:
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()
Nicht vom Dienstcontainer erstellte Dienste
Betrachten Sie folgenden Code:
// Register example service in IServiceCollection.
builder.Services.AddSingleton(new ExampleService());
Für den Code oben gilt:
- Die
ExampleService-Instanz wird nicht vom Dienstcontainer erstellt. - Das Framework löscht die Dienste nicht automatisch.
- Der Entwickler ist für das Löschen der Dienste verantwortlich.
IDisposable-Anleitung für vorübergehende and freigegebene Instanzen
Vorübergehende, eingeschränkte Lebensdauer
Szenario
Für die App ist eine IDisposable-Instanz mit einer vorübergehenden Lebensdauer für eines der folgenden Szenarios erforderlich:
- Die Instanz wird im Stammbereich (Stammcontainer) aufgelöst.
- Die Instanz sollte verworfen werden, bevor der Bereich endet.
Lösung
Verwenden Sie das Factorymuster, um eine Instanz außerhalb des übergeordneten Bereichs zu erstellen. In diesem Fall verfügt die App im Allgemeinen über eine Create Methode, mit der der Konstruktor des endgültigen Typs direkt aufgerufen wird. Wenn der endgültige Typ andere Abhängigkeiten aufweist, kann die Factory folgende Aktionen ausführen:
- Sie kann IServiceProvider im Konstruktor empfangen.
- Verwenden Sie ActivatorUtilities.CreateInstance, um die Instanz außerhalb des Containers zu instanziieren, während Sie den Container für die Abhängigkeiten verwenden.
Freigegebene Instanz (eingeschränkte Lebensdauer)
Szenario
Die App erfordert eine freigegebene IDisposable-Instanz über mehrere Dienste, jedoch sollte die IDisposable-Instanz eine begrenzte Lebensdauer aufweisen.
Lösung
Registrieren Sie die Instanz mit einer bereichsbezogenen Lebensdauer. Verwenden Sie IServiceScopeFactory.CreateScope, um eine neue IServiceScope-Schnittstelle zu erstellen. Verwenden Sie die IServiceProvider-Schnittstelle des Bereichs, um die erforderlichen Dienste abzurufen. Löschen Sie den Bereich, wenn er nicht mehr benötigt wird.
Allgemeine Richtlinien zu IDisposable
- Registrieren Sie keine IDisposable-Instanzen mit einer vorübergehenden Lebensdauer. Verwenden Sie stattdessen das Werksmuster, damit der gelöste Dienst manuell verworfen werden kann, wenn er nicht mehr verwendet wird.
- Lösen Sie keine IDisposable-Instanzen mit vorübergehender oder bereichsbezogener Lebensdauer im Stammbereich auf. Die einzige Ausnahme hierfür ist, wenn die App IServiceProvider erstellt, neu erstellt und verworfen wird, dies ist jedoch kein ideales Muster.
- Wenn eine IDisposable-Abhängigkeit über DI empfangen wird, ist es nicht erforderlich, dass der Empfänger IDisposable selbst implementiert. Der Empfänger der IDisposable-Abhängigkeit darf Dispose auf dieser Abhängigkeit nicht abrufen.
- Verwenden Sie Bereiche, um die Lebensdauer von Diensten zu steuern. Bereiche sind nicht hierarchisch, und es gibt keine besondere Verbindung zwischen den Bereichen.
Weitere Informationen zur Ressourcenbereinigung finden Sie unter Implementieren einer Dispose Methode oder Implementieren einer DisposeAsync Methode. Beachten Sie außerdem das Szenario Vom Container erfasste, verwerfbare vorübergehende Dienste, das sich auf die Ressourcenbereinigung bezieht.
Ersetzen von Standarddienstcontainern
Der integrierte Dienstcontainer dient dazu, die Anforderungen des Frameworks und der meisten Consumer-Apps zu erfüllen. Die Verwendung der integrierten Container wird empfohlen, es sei denn, Sie benötigen ein bestimmtes Feature, das nicht unterstützt wird, zum Beispiel:
- Eigenschaftsinjektion
- Untergeordnete Container
- Benutzerdefinierte Verwaltung der Lebensdauer
-
Func<T>-Unterstützung für die verzögerte Initialisierung - Konventionsbasierte Registrierung
Die folgenden Container von Drittanbietern können mit ASP.NET Core-Apps verwendet werden:
Threadsicherheit
Erstellen Sie threadsichere Singleton-Dienste. Wenn ein Singleton-Dienst eine Abhängigkeit von einem vorübergehenden Dienst hat, erfordert der vorübergehende Dienst möglicherweise auch Threadsicherheit, je nachdem, wie er vom Singleton verwendet wird. Die Factorymethode des einzelnen Diensts, z. B. das zweite Argument für AddSingleton<TService>(IServiceCollection, Func<IServiceProvider,TService>), muss nicht threadsicher sein. Wie bei Konstruktoren vom Typ static erfolgt der Aufruf garantiert nur einmal über einen einzelnen Thread.
Darüber hinaus ist der Prozess der Auflösung von Diensten aus dem integrierten .NET-Abhängigkeitseinfügungscontainer threadsicher.
Sobald ein IServiceProvider oder IServiceScope erstellt wurde, ist es sicher, Dienste gleichzeitig aus mehreren Threads abzurufen.
Hinweis
Threadsicherheit des DI-Containers selbst garantiert nur, dass das Erstellen und Auflösen von Diensten sicher ist. Dadurch werden die aufgelösten Dienstinstanzen nicht selbst threadsicher.
Jeder Dienst (insbesondere Singletons), der gemeinsam genutzten veränderbaren Zustand enthält, muss seine eigene Synchronisierungslogik implementieren, wenn gleichzeitig darauf zugegriffen wird.
Empfehlungen
- Die auf
async/awaitundTaskbasierende Dienstauflösung wird nicht unterstützt. Da C# asynchrone Konstruktoren nicht unterstützt, verwenden Sie asynchrone Methoden, nachdem der Dienst synchron aufgelöst wurde. - Vermeiden Sie das Speichern von Daten und die direkte Konfiguration im Dienstcontainer. Der Einkaufswagen eines Benutzers sollte z. B. normalerweise nicht dem Dienstcontainer hinzugefügt werden. Bei der Konfiguration sollte das Optionsmuster verwendet werden. Gleichermaßen sollten Sie „Daten enthaltende“ Objekte vermeiden, die nur dafür vorhanden sind, den Zugriff auf ein anderes Objekt zuzulassen. Das tatsächlich benötige Element sollte besser über Dependency Injection angefordert werden.
- Vermeiden Sie statischen Zugriff auf Dienste. Vermeiden Sie beispielsweise das Erfassen von IApplicationBuilder.ApplicationServices als statisches Feld oder als Eigenschaft zur Verwendung an einer anderen Stelle.
- DI-Factorys sollten schnell und synchron sein.
- Vermeiden Sie die Verwendung von Dienstlocatormustern. Rufen Sie beispielsweise nicht GetService auf, um eine Dienstinstanz zu erhalten, wenn Sie stattdessen Dependency Injection verwenden können.
- Eine andere Dienstlocator-Variante, die Sie vermeiden sollten, ist die Injektion einer Factory, die zur Laufzeit Abhängigkeiten auflöst. Beide Vorgehensweisen kombinieren Strategien zur Umkehrung der Steuerung.
- Vermeidet Aufrufe von BuildServiceProvider beim Konfigurieren von Diensten. Ein Aufruf von
BuildServiceProvidererfolgt in der Regel, wenn Entwickler*innen einen Dienst auflösen möchten, während ein anderer Dienst registriert wird. Verwenden Sie stattdessen eine Überladung, die denIServiceProviderfür diesen Grund enthält. - Verwerfbare vorübergehende Dienste werden vom Container für die Löschung erfasst. Dadurch kann es zu Arbeitsspeicherverlusten kommen, wenn diese vom obersten Container aus aufgelöst werden.
- Aktivieren Sie die Bereichsüberprüfung, um sicherzustellen, dass die App keine Singletons aufweist, die bereichsbezogene Dienste erfassen. Weitere Informationen finden Sie unter Bereichsvalidierung.
- Verwenden Sie die Singleton-Lebensdauer nur für Dienste mit einem eigenen Zustand, deren Erstellung teuer ist oder die global gemeinsam genutzt werden. Vermeiden Sie die Verwendung der Singleton-Lebensdauer für Dienste, die keinen Zustand selbst haben. Die meisten .NET IoC-Container verwenden "Transient" als Standardbereich. Erwägungen und Nachteile von Singletons:
- Threadsicherheit: Ein Singleton muss auf threadsichere Weise implementiert werden.
- Kopplung: Sie kann andernfalls nicht zusammenhängende Anforderungen koppeln.
- Testprobleme: Gemeinsam genutzter Zustand und Kopplung können Unit-Tests erschweren.
- Auswirkungen auf den Arbeitsspeicher: Ein Singleton kann ein großes Objektdiagramm im Arbeitsspeicher für die Lebensdauer der Anwendung lebendig halten.
- Fehlertoleranz: Wenn ein Singleton oder ein Teil seiner Abhängigkeitsstruktur fehlschlägt, kann es nicht einfach wiederhergestellt werden.
- Neuladen der Konfiguration: Singletons können in der Regel keine "hot reload" von Konfigurationswerten unterstützen.
- Scope-Leakage: Ein Singleton kann unabsichtlich bereichsspezifische oder flüchtige Abhängigkeiten erfassen und sie so auf den Status eines Singletons heben, was zu unbeabsichtigten Nebenwirkungen führt.
- Initialisierungsaufwand: Beim Auflösen eines Diensts muss der IoC-Container die Singleton-Instanz nachschlagen. Wenn sie noch nicht vorhanden ist, muss sie sie auf threadsichere Weise erstellen. Im Gegensatz dazu kann ein flüchtiger, zustandsloser Dienst sehr kostengünstig erstellt und zerstört werden.
Wie alle Empfehlungen können Situationen auftreten, in denen eine Empfehlung ignoriert werden muss. Ausnahmen sind selten und sind meist Sonderfälle innerhalb des Frameworks selbst.
Dependency Injection stellt eine Alternative zu statischen bzw. globalen Objektzugriffsmustern dar. Möglicherweise erkennen Sie die Vorteile von DI nicht, wenn Sie sie mit statischem Objektzugriff kombinieren.
Beispiele für Antimuster
Zusätzlich zu den Richtlinien in diesem Artikel gibt es mehrere Antimuster, die Sie vermeiden sollten. Einige dieser Antimuster sind Erkenntnisse aus der Entwicklung der Runtimes selbst.
Warnung
Dies sind Beispiel für Antimuster. Kopieren Sie den Code nicht, verwenden Sie diese Muster nicht, und vermeiden Sie diese Muster zu allen Kosten.
Vom Container erfasste, verwerfbare vorübergehende Dienste
Wenn Sie vorübergehende Dienste registrieren, die IDisposable implementiert werden, behält der DI-Container standardmäßig diese Verweise bei. Sie werden erst verworfen, wenn der Container verworfen wird, wenn die Anwendung beendet wird, wenn sie aus dem Container aufgelöst wurden, oder bis der Bereich gelöscht wird, wenn er aus einem Bereich aufgelöst wurde. Ein Speicherverlust kann auftreten, wenn das Problem auf der Containerebene behoben wird.
Im vorangehenden Antimuster werden 1.000 ExampleDisposable-Objekte instanziiert und ein Rooting durchgeführt. Sie werden erst dann entsorgt, wenn die Instanz serviceProvider verworfen wird.
Weitere Informationen zum Debuggen von Arbeitsspeicherverlusten finden Sie unter Debuggen eines Arbeitsspeicherverlusts in .NET.
Asynchrone DI-Factorys können Deadlocks verursachen
Der Begriff „DI-Factorys“ bezieht sich auf die Überladungsmethoden, die beim Aufrufen von Add{LIFETIME} vorliegen. Es gibt Überladungen, die ein Func<IServiceProvider, T> akzeptieren, wobei T der registrierte Dienst ist, und der Parameter wird implementationFactory genannt.
implementationFactory kann als Lambda-Ausdruck, lokale Funktion oder Methode angegeben werden. Wenn die Factory asynchron ist und Sie verwenden Task<TResult>.Result, führt dies zu einem Deadlock.
Im vorangehenden Code erhält implementationFactory einen Lambda-Ausdruck, bei dem der Text Task<TResult>.Result für eine Task<Bar>-Rückgabemethode aufruft. Dies führt zu einem Deadlock. Die GetBarAsync-Methode emuliert einfach einen asynchronen Arbeitsvorgang mit Task.Delay und ruft dann GetRequiredService<T>(IServiceProvider) auf.
Weitere Informationen zu asynchronen Vorgängen finden Sie unter Asynchrone Programmierung: Wichtige Informationen und Hinweise. Weitere Informationen zum Debuggen von Deadlocks finden Sie unter Debuggen eines Deadlocks in .NET.
Wenn Sie dieses Antimuster ausführen und der Deadlock auftritt, können Sie die beiden wartenden Threads im Visual Studio-Fenster für parallele Stapel anzeigen. Weitere Informationen finden Sie unter Anzeigen von Threads und Aufgaben im Fenster „Parallele Stapel“.
Unlösbare Abhängigkeit (Captive Dependency)
Der Begriff "gefangene Abhängigkeit", geprägt von Mark Seemann, bezieht sich auf die Fehlkonfiguration von Dienstlebensdauern, bei denen ein längerer Dienst einen kürzeren Dienst in Gefangenschaft hält.
Im vorangehenden Code ist Foo als Singleton registriert, und Bar ist als bereichsbezogen definiert. Diese Konfiguration scheint auf den ersten Blick gültig. Beachten Sie jedoch die Implementierung von Foo.
namespace DependencyInjection.AntiPatterns;
public class Foo(Bar bar)
{
}
Das Foo Objekt erfordert ein Bar Objekt, und da Foo ein Singleton ist und Bar bereichsgebunden ist, handelt es sich um eine Fehlkonfiguration. Wie es ist, wird Foo nur einmal instanziiert, und es hält Bar für seine Lebensdauer, die länger als die beabsichtigte Gültigkeitsdauer von Bar ist. Überlegen Sie die Validierung der Bereiche, indem Sie validateScopes: true an die BuildServiceProvider(IServiceCollection, Boolean) übergeben. Wenn Sie die Bereiche validieren, erhalten Sie eine InvalidOperationException Meldung ähnlich der Folgenden: "Der bereichsbezogene Dienst 'Bar' kann nicht vom Singleton 'Foo' verwendet werden.".
Weitere Informationen finden Sie unter Bereichsvalidierung.
Bereichsbezogener Dienst als Singleton
Wenn Sie bereichsbezogene Dienste verwenden, wird der Dienst zu einem Singleton, wenn Sie keinen Bereich erstellen oder sich nicht innerhalb eines vorhandenen Bereichs befinden.
Im vorangehenden Code wird Bar innerhalb von IServiceScope abgerufen, was zulässig ist. Das Antimuster ist der Abruf von Bar außerhalb des Bereichs. Die Variable ist mit avoid benannt, um anzuzeigen, welcher Beispielabruf fehlerhaft ist.