Richtlinien für die Dependency Injection

Dieser Artikel enthält allgemeine Richtlinien und bewährte Methoden für die Implementierung der Abhängigkeitsinjektion (Dependency Injection, 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 dieser Situation verfügt die App in der Regel über eine Create-Methode, die den Konstruktor des endgültigen Typs direkt aufruft. Wenn der endgültige Typ andere Abhängigkeiten aufweist, kann die Factory folgende Aktionen ausführen:

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 Factorymuster.
  • Lösen Sie keine IDisposable-Instanzen mit vorübergehender oder bereichsbezogener Lebensdauer im Stammbereich auf. Die einzige Ausnahme hierfür besteht in dem Fall, dass die App IServiceProvider erstellt oder neu erstellt und löscht. Dieses Muster ist jedoch nicht ideal.
  • 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 unter 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
  • Einfügung basierend auf dem Namen (.NET 7 und früheren Versionen). Weitere Informationen finden Sie unter Keyed Services.)
  • 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 Singletondienst eine Abhängigkeit von einem vorübergehenden Dienst aufweist, muss der vorübergehende Dienst abhängig von der Verwendungsweise durch das Singleton ebenfalls threadsicher sein.

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.

Empfehlungen

  • Die auf async/await und Task basierende 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 Dienstlocatorvariante, 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 BuildServiceProvider erfolgt 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 den IServiceProvider fü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.

Wie bei allen Empfehlungen treffen Sie möglicherweise auf Situationen, in denen eine Empfehlung ignoriert werden muss. Es gibt nur wenige Ausnahmen, die sich meistens auf besondere Fälle innerhalb des Frameworks beziehen.

Dependency Injection stellt eine Alternative zu statischen bzw. globalen Objektzugriffsmustern dar. Sie werden keinen Nutzen aus der Dependency Injection ziehen können, wenn Sie diese mit dem Zugriff auf statische Objekte 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

Dabei handelt es sich um Beispiele für Antimuster. Kopieren Sie nicht den Code. Sie sollten diese Muster nicht verwenden, sondern unter allen Umständen vermeiden.

Vom Container erfasste, verwerfbare vorübergehende Dienste

Wenn Sie vorübergehende Dienste registrieren, die IDisposable standardmäßig implementieren, behält der DI-Container diese Verweise bei und führt keinen Dispose()-Vorgang durch, bis der Container beim Beenden der Anwendung verworfen wird (bei Auflösung über den Container) oder bis der Bereich verworfen wird (bei Auflösung über einen Bereich). Dadurch kann es zu Arbeitsspeicherverlusten kommen, wenn die Dienste über die Containerebene aufgelöst werden.

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

Im vorangehenden Antimuster werden 1.000 ExampleDisposable-Objekte instanziiert und ein Rooting durchgeführt. Sie werden erst verworfen, wenn die serviceProvider-Instanz 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 Func<IServiceProvider, T> akzeptieren, wobei T den registrierten Dienst und implementationFactory den Parameternamen darstellt. implementationFactory kann als Lambda-Ausdruck, lokale Funktion oder Methode angegeben werden. Wenn die Factory asynchron ist und Sie Task<TResult>.Result verwenden, führt dies zu einem Deadlock.

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

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.

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

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 Captive Dependency wurde von Mark Seemann geprägt und bezieht sich auf eine Fehlkonfiguration der Dienstlebensdauer, bei der ein Dienst mit längerer Lebensdauer einen Dienst mit kürzerer Lebensdauer „gefangen“ hält.

Anti-pattern: Captive dependency. Do not copy!

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 Objekt Foo erfordert ein Objekt Bar, und da Foo ein Singleton ist und Bar bereichsbezogen, handelt es sich hierbei um eine Fehlkonfiguration. In dieser Konstruktion würde Foo nur einmal instanziiert und an Bar für seine Lebensdauer festhalten, welche die vorgesehene Bereichslebensdauer von Bar übersteigt. Sie sollten Bereiche überprüfen, indem Sie validateScopes: true an BuildServiceProvider(IServiceCollection, Boolean) übergeben. Beim Überprüfen der Bereiche erhalten Sie eine InvalidOperationException mit einer Meldung wie „Der bereichsbezogene Dienst 'Bar' kann nicht über den Singleton 'foo' verwendet werden“.

Weitere Informationen finden Sie unter Bereichsvalidierung.

Bereichsbezogener Dienst als Singleton

Wenn Sie bereichsbezogene Dienste verwenden und keinen Bereich erstellen oder innerhalb eines vorhandenen Bereichs bleiben, wird der Dienst zu einem Singleton.

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

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.

Siehe auch