.NET Dependency Injection

.NET unterstützt das Softwareentwurfsmuster der Abhängigkeitsinjektion. Damit kann eine Umkehrung der Steuerung (Inversion of Control, IoC) zwischen Klassen und ihren Abhängigkeiten erreicht werden. 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. Überprüfen Sie die folgende MessageWriter-Klasse mit einer Write-Methode, von der andere Klassen abhängig sind:

public class MessageWriter
{
    public void Write(string message)
    {
        Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
    }
}

Eine Klasse kann eine Instanz der MessageWriter-Klasse erstellen, um die Write-Methode zu nutzen. 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);
        }
    }
}

Die Klasse erstellt die MessageWriter-Klasse und weist eine direkte Abhängigkeit von dieser auf. Hartcodierte Abhängigkeiten (wie im vorherigen Beispiel) sind problematisch und sollten aus folgenden Gründen vermieden werden:

  • Die Worker-Klasse muss geändert werden, um MessageWriter durch eine andere Implementierung zu ersetzen.
  • Wenn MessageWriter über Abhängigkeiten verfügt, müssen diese ebenfalls von der Worker-Klasse konfiguriert werden. In einem großen Projekt mit mehreren Klassen, die von MessageWriter abhä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.

Die Abhängigkeitsinjektion löst dieses Problem mithilfe der folgenden Schritte:

  • 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.
  • Die Injektion des Diensts in den Konstruktor der Klasse, wo er verwendet wird. Das Framework erstellt eine Instanz der Abhängigkeit und entfernt diese, wenn sie nicht mehr benötigt wird.

Die IMessageWriter-Schnittstelle definiert beispielsweise die Write-Methode:

namespace DependencyInjection.Example;

public interface IMessageWriter
{
    void Write(string message);
}

Diese Schnittstelle wird durch einen konkreten Typ (MessageWriter) implementiert:

namespace DependencyInjection.Example;

public class MessageWriter : IMessageWriter
{
    public void Write(string message)
    {
        Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
    }
}

Der Beispielcode registriert den IMessageWriter-Dienst mit dem konkreten Typ MessageWriter. Die AddSingleton-Methode registriert den Dienst mit der Singletonlebensdauer der App. Auf die Dienstlebensdauer wird weiter unten in diesem Artikel eingegangen.

using DependencyInjection.Example;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddHostedService<Worker>();
builder.Services.AddSingleton<IMessageWriter, MessageWriter>();

using IHost host = builder.Build();

host.Run();

Im vorherigen Code führt die Beispiel-App Folgendes aus:

  • Erstellt eine Instanz eines Host-App-Builders.

  • Konfiguriert die Dienste durch Registrierung:

    • Den Worker als gehosteten Dienst. Weitere Informationen finden Sie unter Workdienste in .NET.
    • Die IMessageWriter-Schnittstelle als Singletondienst mit einer entsprechenden Implementierung der MessageWriter-Klasse.
  • Erstellt den Host und führt 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.

namespace DependencyInjection.Example;

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);
        }
    }
}

Bei Verwendung des Abhängigkeitsinjektionsmusters geht der Workerdienst wie folgt vor:

  • Er verwendet nicht den konkreten Typ MessageWriter, sondern nur die von diesem implementierte IMessageWriter-Schnittstelle. Dadurch kann die vom Workerdienst verwendete Implementierung einfacher geändert werden, ohne dass der Workerdienst selbst geändert werden muss.
  • Erstellt keine Instanz von MessageWriter. Die Instanz wird vom DI-Container erstellt.

Die Implementierung der IMessageWriter-Schnittstelle kann mithilfe der integrierten Protokollierungs-API verbessert werden:

namespace DependencyInjection.Example;

public class LoggingMessageWriter(
    ILogger<LoggingMessageWriter> logger) : IMessageWriter
{
    public void Write(string message) =>
        logger.LogInformation("Info: {Msg}", message);
}

Die aktualisierte AddSingleton-Methode registriert die neue IMessageWriter-Implementierung:

builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();

Der Typ HostApplicationBuilder (builder) ist Teil des NuGet-Pakets Microsoft.Extensions.Hosting.

LoggingMessageWriter hängt von der ILogger<TCategoryName>-Schnittstelle ab, die im Konstruktor angefordert wird. ILogger<TCategoryName> ist ein vom Framework bereitgestellter Dienst.

Die Abhängigkeitsinjektion wird häufig als Verkettung verwendet. Jede angeforderte Abhängigkeit fordert wiederum ihre eigenen Abhängigkeiten an. Der Container löst die Abhängigkeiten im Diagramm auf und gibt den vollständig aufgelösten Dienst zurück. Die gesammelten aufzulösenden Abhängigkeiten werden als Abhängigkeitsstruktur, Abhängigkeitsdiagramm oder Objektdiagramm bezeichnet.

Der Container löst ILogger<TCategoryName> unter Verwendung der (generischen) offenen Typen auf, wodurch nicht mehr jeder (generische) konstruierte Typ registriert werden muss:

Im Kontext der Abhängigkeitsinjektion ist ein Dienst:

  • in der Regel ein Objekt, das einen Dienst für andere Objekte bereitstellt, z. B. den IMessageWriter-Dienst.
  • kein Webdienst, obwohl ein Dienst einen Webdienst verwenden kann

Das Framework bietet eine stabiles Protokollierungssystem. Die in den vorherigen Beispielen veranschaulichten IMessageWriter-Implementierungen wurden geschrieben, um die grundlegende Dependency Injection zu demonstrieren, nicht zum Implementieren der Protokollierung. Die meisten Apps müssen keine Protokollierungen schreiben. Der folgende Code veranschaulicht die Verwendung der Standardprotokollierung, bei der nur der Worker als gehosteter Dienst AddHostedService registriert werden muss:

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;

    public Worker(ILogger<Worker> logger) =>
        _logger = logger;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            await Task.Delay(1_000, stoppingToken);
        }
    }
}

Bei Verwendung des vorangehenden Codes muss Program.cs nicht aktualisiert werden, weil die Protokollierung vom Framework bereitgestellt wird.

Ermittlungsregeln für mehrere Konstruktoren

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. Sehen Sie sich den folgenden C#-Beispieldienst an:

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // omitted for brevity
    }

    public ExampleService(FooService fooService, BarService barService)
    {
        // omitted for brevity
    }
}

Gehen Sie im vorangehenden Code davon aus, dass die Protokollierung hinzugefügt wurde und vom Dienstanbieter aufgelöst werden kann, die Typen FooServiceund BarService jedoch nicht. Der Konstruktor mit dem Parameter ILogger<ExampleService> wird verwendet, um die ExampleService-Instanz aufzulösen. Es gibt zwar einen Konstruktor, der mehr Parameter definiert, aber die Typen FooService und BarService sind nicht per Abhängigkeitsinjektion auflösbar.

Wenn beim Ermitteln von Konstruktoren Mehrdeutigkeiten bestehen, wird eine Ausnahme ausgelöst. Sehen Sie sich den folgenden C#-Beispieldienst an:

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(ILogger<ExampleService> logger)
    {
        // omitted for brevity
    }

    public ExampleService(IOptions<ExampleOptions> options)
    {
        // omitted for brevity
    }
}

Warnung

Der ExampleService-Code mit mehrdeutigen, per Abhängigkeitsinjektion auflösbaren Typparametern würde eine Ausnahme auslösen. Verwenden Sie diese Vorgehensweise nicht. Sie soll lediglich demonstrieren, was mit „mehrdeutigen, per Abhängigkeitsinjektion auflösbaren Typen“ gemeint ist.

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, wird eine Ausnahme ausgelöst, weil die beiden Konstruktoren mehrdeutig sind.

Sie können Mehrdeutigkeiten vermeiden, indem Sie einen Konstruktor definieren, der stattdessen beide per Abhängigkeitsinjektion auflösbaren Typen akzeptiert:

public class ExampleService
{
    public ExampleService()
    {
    }

    public ExampleService(
        ILogger<ExampleService> logger,
        IOptions<ExampleOptions> options)
    {
        // omitted for brevity
    }
}

Registrieren von Dienstgruppen mit Erweiterungsmethoden

Microsoft.Extensions verwendet eine Konvention zum Registrieren einer Gruppe verwandter Dienste. Die Konvention besteht darin, eine einzelne Add{GROUP_NAME}-Erweiterungsmethode zum Registrieren aller Dienste zu verwenden, die von einem Frameworkfeature benötigt werden. Beispielsweise registriert die Erweiterungsmethode AddOptions alle Dienste, die für die Verwendung von Optionen erforderlich sind.

Von Frameworks bereitgestellte Dienste

Wenn Sie eines der verfügbaren Host- oder App-Buildermuster verwenden, werden Standardeinstellungen angewendet, und Dienste werden vom Framework registriert. Dies sind einige der beliebtesten Muster für Host- und App-Builder:

Nach dem Erstellen eines Builders aus einer dieser APIs enthält die IServiceCollection vom Framework definierte Dienste, je nachdem, wie der Host konfiguriert wurde. Für Apps, die auf .NET-Vorlagen basieren, registriert das Framework möglicherweise Hunderte von Diensten.

In der folgenden Tabelle finden Sie eine kleine Stichprobe der vom Framework registrierten Dienste:

Diensttyp Gültigkeitsdauer
Microsoft.Extensions.DependencyInjection.IServiceScopeFactory Singleton
IHostApplicationLifetime Singleton
Microsoft.Extensions.Logging.ILogger<TCategoryName> Singleton
Microsoft.Extensions.Logging.ILoggerFactory Singleton
Microsoft.Extensions.ObjectPool.ObjectPoolProvider Singleton
Microsoft.Extensions.Options.IConfigureOptions<TOptions> Transient (vorübergehend)
Microsoft.Extensions.Options.IOptions<TOptions> Singleton
System.Diagnostics.DiagnosticListener Singleton
System.Diagnostics.DiagnosticSource Singleton

Dienstlebensdauer

Dienste können mit einer den folgenden Lebensdaueroptionen registriert werden:

In den folgenden Abschnitten werden die einzelnen genannten Lebensdaueroptionen beschrieben. Wählen Sie eine geeignete Lebensdauer für jeden registrierten Dienst aus.

Transient (vorübergehend)

Kurzlebige Dienste werden bei jeder Anforderung aus dem Dienstcontainer neu erstellt. Um einen Dienst als vorübergehend zu registrieren, rufen Sie AddTransient auf.

In Apps, die Anforderungen verarbeiten, werden vorübergehende Dienste am Ende der Anforderung verworfen. Diese Lebensdauer verursacht Zuweisungen pro Anforderung, da Dienste jedes Mal aufgelöst und erstellt werden. Weitere Informationen finden Sie unter Richtlinien für die Dependency Injection: IDisposable-Leitfaden für vorübergehende und freigegebene Instanzen.

Bereichsbezogen

Bei Webanwendungen weist eine bereichsbezogene Lebensdauer darauf hin, dass Dienste einmal pro Clientanforderung (Verbindung) erstellt werden. Bereichsbezogene Dienste werden mit AddScoped registriert.

In Apps, die Anforderungen verarbeiten, werden bereichsbezogene Dienste am Ende der Anforderung verworfen.

Wenn Sie Entity Framework Core verwenden, registriert die AddDbContext-Erweiterungsmethode standardmäßig DbContext-Typen mit einer begrenzten Lebensdauer.

Hinweis

Lösen Sie einen bereichsbezogenen Dienst nicht über einen Singleton auf, und achten Sie darauf, dies auch nicht indirekt auszuführen, z. B. über einen vorübergehenden Dienst. Möglicherweise weist der Dienst bei der Verarbeitung nachfolgender Anforderungen einen falschen Status auf. Folgendes ist zulässig:

  • das Auflösen eines Singletondiensts aus einem bereichsbezogenen oder temporären Diensts.
  • das Auflösen eines bereichsbezogenen Diensts aus einem anderen bereichsbezogenen oder temporären Dienst

Standardmäßig wird in der Entwicklungsumgebung eine Ausnahme ausgelöst, wenn ein Dienst von einem anderen Dienst mit längerer Lebensdauer aufgelöst wird. Weitere Informationen finden Sie unter Bereichsvalidierung.

Singleton

Dienste mit Singletonlebensdauer werden erstellt:

  • wenn sie zum ersten Mal angefordert werden
  • vom Entwickler, wenn eine Implementierungsinstanz direkt im Container bereitgestellt wird (selten benötigter Ansatz)

Jede nachfolgende Anforderung der Dienstimplementierung aus dem Container für die Abhängigkeitsinjektion verwendet dieselbe Instanz. Wenn für die App Singletonverhalten erforderlich ist, sollte der Dienstcontainer die Dienstlebensdauer verwalten. Wenn Sie das Singletonentwurfsmuster implementieren, sollten Sie keinen Code zum Löschen des Singleton angeben. Dienste sollten nicht durch den Code gelöscht werden können, der den Dienst aus einem Container aufgelöst hat. Wenn ein Typ oder eine Factory als Singleton registriert ist, wird das Singleton automatisch vom Container verworfen.

Registrieren Sie Singletondienste mit AddSingleton. Singletondienste müssen threadsicher sein und werden häufig in zustandslosen Diensten verwendet.

In Apps, die Anforderungen verarbeiten, werden Singletondienste gelöscht, wenn ServiceProvider beim Herunterfahren gelöscht wird. Da der Arbeitsspeicher erst freigegeben wird, wenn die App heruntergefahren wird, müssen Sie den Arbeitsspeicherverbrauch eines Singletons berücksichtigen.

Dienstregistrierungsmethoden

Das Framework stellt Erweiterungsmethoden für die Dienstregistrierung bereit, die in bestimmten Szenarios hilfreich sind:

Methode Automatische
Objekt
bereinigung
Mehrere
Implementierungen
Argumentübergabe
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>()

Beispiel:

services.AddSingleton<IMyDep, MyDep>();
Ja Ja Nein
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION})

Beispiele:

services.AddSingleton<IMyDep>(sp => new MyDep());
services.AddSingleton<IMyDep>(sp => new MyDep(99));
Ja Ja Ja
Add{LIFETIME}<{IMPLEMENTATION}>()

Beispiel:

services.AddSingleton<MyDep>();
Ja Nr. Nein
AddSingleton<{SERVICE}>(new {IMPLEMENTATION})

Beispiele:

services.AddSingleton<IMyDep>(new MyDep());
services.AddSingleton<IMyDep>(new MyDep(99));
Nein Ja Ja
AddSingleton(new {IMPLEMENTATION})

Beispiele:

services.AddSingleton(new MyDep());
services.AddSingleton(new MyDep(99));
Nein Nein Ja

Weitere Informationen zum Löschen von Typen finden Sie im Abschnitt Löschen von Diensten.

Das Registrieren eines Diensts mit nur einem Implementierungstyp entspricht dem Registrieren dieses Diensts mit demselben Implementierungs- und Diensttyp. Aus diesem Grund können nicht mehrere Implementierungen eines Diensts mithilfe von Methoden registriert werden, die keinen expliziten Diensttyp erwarten. Solche Methoden können mehrere Instanzen eines Diensts registrieren, die dann jedoch alle denselben Implementierungstyp aufweisen.

Alle oben genannten Dienstregistrierungsmethoden können zum Registrieren mehrerer Dienstinstanzen desselben Diensttyps verwendet werden. Im folgenden Beispiel wird AddSingleton zweimal mit IMessageWriter als Diensttyp aufgerufen. Mit dem zweiten Aufruf von AddSingleton wird der vorhandene Singleton überschrieben, wenn er als IMessageWriter aufgelöst wurde. Wenn mehrere Dienste über IEnumerable<IMessageWriter> aufgelöst werden, wird der neue Singleton hinzugefügt und der alte beibehalten. Dienste werden beim Auflösen mit IEnumerable<{SERVICE}> in der Reihenfolge angezeigt, in der sie registriert wurden.

using ConsoleDI.IEnumerableExample;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddSingleton<IMessageWriter, ConsoleMessageWriter>();
builder.Services.AddSingleton<IMessageWriter, LoggingMessageWriter>();
builder.Services.AddSingleton<ExampleService>();

using IHost host = builder.Build();

_ = host.Services.GetService<ExampleService>();

await host.RunAsync();

Im vorherigen Beispielquellcode werden zwei IMessageWriter-Implementierungen registriert.

using System.Diagnostics;

namespace ConsoleDI.IEnumerableExample;

public sealed class ExampleService
{
    public ExampleService(
        IMessageWriter messageWriter,
        IEnumerable<IMessageWriter> messageWriters)
    {
        Trace.Assert(messageWriter is LoggingMessageWriter);

        var dependencyArray = messageWriters.ToArray();
        Trace.Assert(dependencyArray[0] is ConsoleMessageWriter);
        Trace.Assert(dependencyArray[1] is LoggingMessageWriter);
    }
}

Der ExampleService definiert zwei Konstruktorparameter, einen einzelnen IMessageWriter und einen IEnumerable<IMessageWriter>. Der einzelne IMessageWriter ist die letzte Implementierung, die registriert wurde, während IEnumerable<IMessageWriter> alle registrierten Implementierungen darstellt.

Das Framework stellt außerdem TryAdd{LIFETIME}-Erweiterungsmethoden bereit, die den Dienst nur registrieren, wenn noch keine Implementierung registriert ist.

Im folgenden Beispiel registriert der AddSingleton-Aufruf ConsoleMessageWriter als Implementierung für IMessageWriter. Der Aufruf von TryAddSingleton hat keine Auswirkung, weil IMessageWriter bereits eine registrierte Implementierung aufweist:

services.AddSingleton<IMessageWriter, ConsoleMessageWriter>();
services.TryAddSingleton<IMessageWriter, LoggingMessageWriter>();

TryAddSingleton hat keine Auswirkungen, da es bereits hinzugefügt wurde. Der Vorgang „try“ ist nicht durchführbar. Der ExampleService führt folgende Assert-Anweisungen aus:

public class ExampleService
{
    public ExampleService(
        IMessageWriter messageWriter,
        IEnumerable<IMessageWriter> messageWriters)
    {
        Trace.Assert(messageWriter is ConsoleMessageWriter);
        Trace.Assert(messageWriters.Single() is ConsoleMessageWriter);
    }
}

Weitere Informationen finden Sie unter

Die TryAddEnumerable(ServiceDescriptor)-Methoden registrieren den Dienst nur, wenn noch keine Implementierung desselben Typs vorhanden ist. Mehrere Dienste werden über IEnumerable<{SERVICE}> aufgelöst. Fügen Sie beim Registrieren von Diensten eine Instanz hinzu, wenn nicht bereits eine Instanz desselben Typs hinzugefügt wurde. Bibliotheksautoren verwenden TryAddEnumerable, um zu vermeiden, dass mehrere Kopien einer Implementierung im Container registriert werden.

Im folgenden Beispiel registriert der erste TryAddEnumerable-Aufruf MessageWriter als Implementierung für IMessageWriter1. Der zweite Aufruf registriert MessageWriter für IMessageWriter2. Der dritte Aufruf hat keine Auswirkungen, da IMessageWriter1 bereits eine registrierte Implementierung von MessageWriter aufweist:

public interface IMessageWriter1 { }
public interface IMessageWriter2 { }

public class MessageWriter : IMessageWriter1, IMessageWriter2
{
}

services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter1, MessageWriter>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter2, MessageWriter>());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IMessageWriter1, MessageWriter>());

Bei der Dienstregistrierung ist die Reihenfolge unerheblich, wenn mehrere Implementierungen desselben Typs registriert werden.

IServiceCollection ist eine Sammlung von ServiceDescriptor-Objekten. Im folgenden Beispiel wird gezeigt, wie ein Dienst durch Erstellen und Hinzufügen von ServiceDescriptor registriert wird:

string secretKey = Configuration["SecretKey"];
var descriptor = new ServiceDescriptor(
    typeof(IMessageWriter),
    _ => new DefaultMessageWriter(secretKey),
    ServiceLifetime.Transient);

services.Add(descriptor);

Die integrierten Add{LIFETIME}-Methoden verwenden denselben Ansatz. Ein Beispiel finden Sie im Quellcode für AddScoped.

Verhalten von Constructor Injection

Dienste können mithilfe der folgenden Ansätze aufgelöst werden:

Konstruktoren können Argumente akzeptieren, die nicht durch Abhängigkeitsinjektion bereitgestellt werden. Die Argumente müssen jedoch Standardwerte zuweisen.

Wenn Dienste durch IServiceProvider oder ActivatorUtilities aufgelöst werden, benötigt die Konstruktorinjektion einen öffentlichen Konstruktor.

Wenn Dienste durch ActivatorUtilities aufgelöst werden, erfordert Constructor Injection, dass nur ein anwendbarer Konstruktor vorhanden ist. Konstruktorüberladungen werden unterstützt. Es darf jedoch nur eine Überladung vorhanden sein, deren Argumente alle durch Dependency Injection erfüllt werden können.

Bereichsvalidierung

Wenn die App in der Development-Umgebung ausgeführt wird und CreateApplicationBuilder aufruft, um den Host zu erstellen, führt der Standarddienstanbieter Überprüfungen aus, um sicherzustellen, dass:

  • bereichsbezogene Dienste nicht vom Stammdienstanbieter aufgelöst werden.
  • bereichsbezogene Dienste nicht in Singletons eingefügt werden.

Der Stammdienstanbieter wird erstellt, wenn BuildServiceProvider aufgerufen wird. Die Lebensdauer des Stammdienstanbieters entspricht der Lebensdauer der App, wenn der Anbieter beim Start der App erstellt und beim Herunterfahren gelöscht wird.

Bereichsbezogene Dienste werden von dem Container verworfen, der sie erstellt hat. Wenn ein bereichsbezogener Dienst im Stammcontainer erstellt wird, wird die Lebensdauer quasi auf Singleton heraufgestuft, da er nur vom Stammcontainer verworfen wird, wenn die App beendet wird. Die Überprüfung bereichsbezogener Dienste erfasst diese Situationen, wenn BuildServiceProvider aufgerufen wird.

Bereichsszenarios

IServiceScopeFactory ist immer als Singleton registriert, aber IServiceProvider kann je nach Lebensdauer der enthaltenden Klasse variieren. Wenn Sie z. B. Dienste aus einem Bereich auflösen und einer dieser Dienste eine IServiceProvider-Instanz übernimmt, handelt es sich um eine bereichsbezogene Instanz.

Fügen Sie nicht die Dienstabhängigkeiten per Konstruktorinjektion ein, um Bereichsdienste innerhalb von Implementierungen von IHostedService, wie BackgroundService, zu erreichen. 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.
  • Er erstellt IServiceScope zum Auflösen zusätzlicher 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

Ab .NET 8 werden die Dienstregistrierungen und die Lookupvorgänge basierend auf einem Schlüssel unterstützt. Das bedeutet, dass es möglich ist, mehrere Dienste mit einem anderen Schlüssel 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");

Dies key ist nicht beschränkt auf string, es kann beliebig object sein, solange der Typ ordnungsgemäß Equals 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...
    }
}

Siehe auch