Grundlegendes zur Dependency Injection in .NET
In diesem Artikel erstellen Sie eine .NET-Konsolen-App, die manuell eine ServiceCollection und entsprechende ServiceProvidererstellt. Sie erfahren, wie Sie Dienste registrieren und mithilfe der Dependency Injection (DI) auflösen. In diesem Artikel wird das NuGet-Paket Microsoft.Extensions.DependencyInjection verwendet, um die Grundlagen von DI in .NET zu veranschaulichen.
Hinweis
In diesem Artikel werden die generischen Hostfeatures nicht genutzt. Eine ausführlichere Anleitung finden Sie unter Verwenden der Dependency Injection in .NET.
Erste Schritte
Erstellen Sie zunächst eine neue .NET-Konsolenanwendung mit dem Namen DI.Basics. Auf einige der am häufigsten verwendeten Ansätze zum Erstellen eines Konsolenprojekts wird in der folgenden Liste verwiesen:
- Visual Studio: Menü Datei > Neu > Projekt .
- Visual Studio Code und die C# Dev Kit-Erweiterung: Menüoption Projektmappen-Explorer.
- .NET CLI:
dotnet new console
Befehl im Terminal.
Sie müssen den Paketverweis auf Microsoft.Extensions.DependencyInjection in der Projektdatei hinzufügen. Stellen Sie unabhängig vom Ansatz sicher, dass das Projekt dem folgenden XML der Datei DI.Basics.csproj ähnelt:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
</ItemGroup>
</Project>
Allgemeine Informationen zur Dependency Injection
Die Dependency Injection ist ein Entwurfsmuster, das es Ihnen ermöglicht, hartcodierte Abhängigkeiten zu entfernen und ihre Anwendung besser zu verwalten und zu testen. DI ist eine Technik zum Erreichen von Inversion of Control (IoC) zwischen Klassen und ihren Abhängigkeiten.
Die Abstraktionen für DI in .NET werden im NuGet-Paket Microsoft.Extensions.DependencyInjection.Abstractions definiert:
- IServiceCollection: Gibt einen Vertrag für eine Sammlung von Dienstdeskriptoren an.
- IServiceProvider: Definiert einen Mechanismus zum Abrufen eines Dienstobjekts.
- ServiceDescriptor: Beschreibt einen Dienst mit seinem Diensttyp, seiner Implementierung und Lebensdauer.
In .NET wird die Dependency Injection (DI) verwaltet, indem Dienste hinzugefügt und über IServiceCollection
konfiguriert werden. Nachdem Dienste registriert wurden, wird die IServiceProvider
-Instanz durch Aufrufen der Methode BuildServiceProvider erstellt. Der IServiceProvider
dient als Container aller registrierten Dienste und wird verwendet, um Dienste aufzulösen.
Erstellen der example-Dienste
Nicht alle Dienste werden gleich erstellt. Einige Dienste erfordern jedes Mal eine neue Instanz, wenn der Dienstcontainer sie abruft (transient), während andere über Anforderungen (scoped) oder für die gesamte Lebensdauer der App (singleton) freigegeben werden sollten. Weitere Informationen zur Lebensdauer von Diensten finden Sie unter Dienstlebensdauern.
Ebenso machen einige Dienste nur einen konkreten Typ verfügbar, während andere als Vertrag zwischen einer Schnittstelle und einem Implementierungstyp ausgedrückt werden. Sie erstellen mehrere Varianten von Diensten, um diese Konzepte zu veranschaulichen.
Erstellen Sie eine neue C#-Datei mit dem Namen IConsole.cs, und fügen Sie den folgenden Code ein:
public interface IConsole
{
void WriteLine(string message);
}
Diese Datei definiert eine IConsole
-Schnittstelle, die eine einzelne Methode – WriteLine
– verfügbar macht. Erstellen Sie als Nächstes eine neue C#-Datei mit dem Namen DefaultConsole.cs, und fügen Sie den folgenden Code ein:
internal sealed class DefaultConsole : IConsole
{
public bool IsEnabled { get; set; } = true;
void IConsole.WriteLine(string message)
{
if (IsEnabled is false)
{
return;
}
Console.WriteLine(message);
}
}
Der vorangehende Code stellt die Standardimplementierung der Schnittstelle IConsole
dar. Die Methode WriteLine
schreibt bedingt auf der Grundlage der Eigenschaft IsEnabled
in die Konsole.
Tipp
Die Benennung einer Implementierung ist eine Wahl, über die sich Ihr Entwicklerteam einigen sollte. Das Präfix Default
ist eine gängige Konvention, um eine Standardimplementierung einer Schnittstelle anzugeben, ist jedoch nicht erforderlich.
Erstellen Sie als Nächstes eine IGreetingService.cs-Datei, und fügen Sie den folgenden C#-Code hinzu:
public interface IGreetingService
{
string Greet(string name);
}
Fügen Sie dann eine neue C#-Datei mit dem Namen DefaultGreetingService.cs und darin den folgenden Code hinzu:
internal sealed class DefaultGreetingService(
IConsole console) : IGreetingService
{
public string Greet(string name)
{
var greeting = $"Hello, {name}!";
console.WriteLine(greeting);
return greeting;
}
}
Der vorangehende Code stellt die Standardimplementierung der Schnittstelle IGreetingService
dar. Für die Dienstimplementierung ist IConsole
als primärer Konstruktorparameter erforderlich. Die Greet
-Methode:
- Erstellt eine
greeting
mit demname
. - Ruft die Methode
WriteLine
für die InstanzIConsole
auf. - Gibt
greeting
an den Aufrufer zurück.
Der letzte zu erstellende Dienst ist die Datei FarewellService.cs. Fügen Sie den folgenden C#-Code hinzu, bevor Sie fortfahren:
public class FarewellService(IConsole console)
{
public string SayGoodbye(string name)
{
var farewell = $"Goodbye, {name}!";
console.WriteLine(farewell);
return farewell;
}
}
FarewellService
stellt einen konkreten Typ und keine Schnittstelle dar. Er sollte als public
deklariert werden, damit er den Verbrauchern zugänglich ist. Im Gegensatz zu anderen Dienstimplementierungstypen, die als internal
und sealed
deklariert wurden, veranschaulicht dieser Code, dass nicht alle Dienste Schnittstellen sein müssen. Außerdem wird gezeigt, dass Dienstimplementierungen sealed
werden können, um die Vererbung zu verhindern und internal
, um den Zugriff auf die Assembly einzuschränken.
Aktualisieren Sie die Klasse Program
.
Ersetzen Sie in der Datei Program.cs den vorhandenen Code durch den folgenden C#-Code:
using Microsoft.Extensions.DependencyInjection;
// 1. Create the service collection.
var services = new ServiceCollection();
// 2. Register (add and configure) the services.
services.AddSingleton<IConsole>(
implementationFactory: static _ => new DefaultConsole
{
IsEnabled = true
});
services.AddSingleton<IGreetingService, DefaultGreetingService>();
services.AddSingleton<FarewellService>();
// 3. Build the service provider from the service collection.
var serviceProvider = services.BuildServiceProvider();
// 4. Resolve the services that you need.
var greetingService = serviceProvider.GetRequiredService<IGreetingService>();
var farewellService = serviceProvider.GetRequiredService<FarewellService>();
// 5. Use the services
var greeting = greetingService.Greet("David");
var farewell = farewellService.SayGoodbye("David");
Im vorangehenden aktualisierten Code wird die Vorgehensweise veranschaulicht:
- Erstellen Sie eine neue
ServiceCollection
-Instanz. - Registrieren und Konfigurieren von Diensten in der
ServiceCollection
:- Geben Sie
IConsole
mit der Implementierungsfactoryüberladung einenDefaultConsole
-Typ zurück, derIsEnabled
auf „true“ festgelegt ist. - Der
IGreetingService
wird mit einem entsprechenden Implementierungstyp des TypsDefaultGreetingService
hinzugefügt. - Der
FarewellService
wird als konkreter Typ hinzugefügt.
- Geben Sie
- Erstellen Sie den
ServiceProvider
aus derServiceCollection
. - Lösen Sie die Dienste
IGreetingService
undFarewellService
auf. - Verwenden Sie die aufgelösten Dienste, um eine Person namens
David
zu begrüßen und zu verabschieden.
Wenn Sie die Eigenschaft IsEnabled
von DefaultConsole
auf false
aktualisieren, unterlassen die Methoden Greet
und SayGoodbye
das Schreiben der daraus resultierenden Nachrichten in die Konsole. Eine solche Änderung zeigt, dass der Dienst IConsole
in die Dienste IGreetingService
und FarewellService
als Abhängigkeit eingebunden ist, die das Verhalten dieser Anwendungen beeinflusst.
Alle diese Dienste werden als Singletons registriert, obwohl sie für dieses Beispiel identisch funktionieren, wenn sie als transient oder scoped Dienste registriert wurden.
Wichtig
In diesem example spielen die Lebensdauern der Dienste keine Rolle, aber in einer realen Anwendung sollten Sie die Lebensdauer jedes Dienstes sorgfältig prüfen.
Ausführen der Beispiel-App
Um die Beispielanwendung auszuführen, drücken Sie entweder F5 in Visual Studio, Visual Studio Code oder führen Sie den Befehl dotnet run
im Terminal aus. Wenn die Anwendung abgeschlossen ist, sollten Sie die folgende Ausgabe sehen:
Hello, David!
Goodbye, David!
Dienstdeskriptoren
Die am häufigsten verwendeten APIs zum Hinzufügen von Diensten zum ServiceCollection
sind generische Erweiterungsmethoden mit Lebenszeitnamen, wie z. B.:
AddSingleton<TService>
AddTransient<TService>
AddScoped<TService>
Bei diesen Methoden handelt es sich um Komfortmethoden, die eine ServiceDescriptor-Instanz erstellen und sie zu ServiceCollection
hinzufügen. Der ServiceDescriptor
ist eine einfache Klasse, die einen Dienst mit seinem Diensttyp, seiner Implementierungsart und seiner Lebensdauer beschreibt. Er kann auch Implementierungsfabriken und Instanzen beschreiben.
Für jeden der Dienste, die Sie in ServiceCollection
registriert haben, könnten Sie stattdessen die Methode Add
mit einer ServiceDescriptor
-Instanz direkt aufrufen. Betrachten Sie die folgenden Beispiele:
services.Add(ServiceDescriptor.Describe(
serviceType: typeof(IConsole),
implementationFactory: static _ => new DefaultConsole
{
IsEnabled = true
},
lifetime: ServiceLifetime.Singleton));
Der vorangehende Code entspricht der Registrierung des Diensts IConsole
in der ServiceCollection
. Die Methode Add
wird verwendet, um eine ServiceDescriptor
-Instanz hinzuzufügen, die den Dienst IConsole
beschreibt. Die statische Methode ServiceDescriptor.Describe
delegiert an verschiedene ServiceDescriptor
-Konstruktoren. Berücksichtigen Sie den entsprechenden Code für den Dienst IGreetingService
:
services.Add(ServiceDescriptor.Describe(
serviceType: typeof(IGreetingService),
implementationType: typeof(DefaultGreetingService),
lifetime: ServiceLifetime.Singleton));
Der vorangehende Code beschreibt den Dienst IGreetingService
mit dem Diensttyp, dem Implementierungstyp und der Lebensdauer. Berücksichtigen Sie abschließend den entsprechenden Code für den Dienst FarewellService
:
services.Add(ServiceDescriptor.Describe(
serviceType: typeof(FarewellService),
implementationType: typeof(FarewellService),
lifetime: ServiceLifetime.Singleton));
Der vorangehende Code beschreibt den konkreten Typ FarewellService
sowohl als Dienst- als auch als Implementierungstypen. Der Dienst wird als singleton-Dienst registriert.