Tutorial: Verwenden der Dependency Injection in .NET

In diesem Tutorial erfahren Sie, wie die Dependency Injection (DI) in .NET eingesetzt wird. Die Abhängigkeitsinjektion (Dependency Injection, DI) wird mit Microsoft-Erweiterungen verwaltet, indem Dienste hinzugefügt und über eine IServiceCollection-Schnittstelle konfiguriert werden. Die Schnittstelle IHost macht die Instanz IServiceProvider verfügbar, die als Container für alle registrierten Dienste fungiert.

In diesem Tutorial lernen Sie Folgendes:

  • Erstellen einer .NET-Konsolen-App, die die Dependency Injection verwendet
  • Erstellen und Konfigurieren eines generischen Hosts
  • Schreiben mehrerer Schnittstellen und entsprechender Implementierungen
  • Verwenden der Dienstlebensdauer und der Bereichsdefinitionen für die DI

Voraussetzungen

  • SDK für .NET Core 3.1 oder höher
  • Erfahrung mit dem Erstellen neuer .NET-Anwendungen und der Installation von NuGet-Paketen

Erstellen einer neuen Konsolenanwendung

Erstellen Sie über den Befehl dotnet new oder den Assistenten für neue Projekte auf der IDE eine neue .NET-Konsolenanwendung namens ConsoleDI.Example . Fügen Sie das NuGet-Paket Microsoft.Extensions.Hosting zum Projekt hinzu.

Ihre neue Projektdatei für die Konsolen-App sollte wie folgt aussehen:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>true</ImplicitUsings>
    <RootNamespace>ConsoleDI.Example</RootNamespace>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
  </ItemGroup>

</Project>

Wichtig

In diesem Beispiel ist das NuGet-Paket Microsoft.Extensions.Hosting erforderlich, um die App zu erstellen und auszuführen. Einige Metapakete enthalten möglicherweise das Microsoft.Extensions.Hosting-Paket. In diesem Fall ist kein expliziter Paketverweis erforderlich.

Hinzufügen von Schnittstellen

Mithilfe dieser Beispiel-App erfahren Sie, wie die Abhängigkeitsinjektion die Dienstlebensdauer verwaltet. Sie erstellen mehrere Schnittstellen, die unterschiedliche Dienstlebensdauern darstellen. Fügen Sie dem Stammverzeichnis des Projekts die folgenden Schnittstellen hinzu:

IReportServiceLifetime.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IReportServiceLifetime
{
    Guid Id { get; }

    ServiceLifetime Lifetime { get; }
}

Die IReportServiceLifetime-Schnittstelle definiert Folgendes:

  • Guid Id-Eigenschaft, die den eindeutigen Bezeichner des Diensts darstellt
  • ServiceLifetime-Eigenschaft, die die Dienstlebensdauer darstellt

IExampleTransientService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleTransientService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Transient;
}

IExampleScopedService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleScopedService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Scoped;
}

IExampleSingletonService.cs

using Microsoft.Extensions.DependencyInjection;

namespace ConsoleDI.Example;

public interface IExampleSingletonService : IReportServiceLifetime
{
    ServiceLifetime IReportServiceLifetime.Lifetime => ServiceLifetime.Singleton;
}

Alle untergeordneten Schnittstellen von IReportServiceLifetime implementieren IReportServiceLifetime.Lifetime mit einem Standardwert. IExampleTransientService implementiert beispielsweise IReportServiceLifetime.Lifetime explizit mit dem ServiceLifetime.Transient-Wert.

Hinzufügen von Standardimplementierungen

Die Beispielimplementierungen initialisieren ihre Id-Eigenschaft mit dem Ergebnis von Guid.NewGuid(). Fügen Sie die folgenden Standardimplementierungsklassen für die verschiedenen Dienste zum Projektstammverzeichnis hinzu:

ExampleTransientService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleTransientService : IExampleTransientService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

ExampleScopedService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleScopedService : IExampleScopedService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

ExampleSingletonService.cs

namespace ConsoleDI.Example;

internal sealed class ExampleSingletonService : IExampleSingletonService
{
    Guid IReportServiceLifetime.Id { get; } = Guid.NewGuid();
}

Jede Implementierung wird als internal sealed definiert und implementiert die entsprechende Schnittstelle. ExampleSingletonService implementiert beispielsweise IExampleSingletonService.

Hinzufügen eines Diensts, der die DI erfordert

Fügen Sie der Konsolen-App die folgende Dienstlebensdauer-Berichtserstellungsklasse hinzu, die als Dienst fungiert:

ServiceLifetimeReporter.cs

namespace ConsoleDI.Example;

internal sealed class ServiceLifetimeReporter(
    IExampleTransientService transientService,
    IExampleScopedService scopedService,
    IExampleSingletonService singletonService)
{
    public void ReportServiceLifetimeDetails(string lifetimeDetails)
    {
        Console.WriteLine(lifetimeDetails);

        LogService(transientService, "Always different");
        LogService(scopedService, "Changes only with lifetime");
        LogService(singletonService, "Always the same");
    }

    private static void LogService<T>(T service, string message)
        where T : IReportServiceLifetime =>
        Console.WriteLine(
            $"    {typeof(T).Name}: {service.Id} ({message})");
}

ServiceLifetimeReporter definiert einen Konstruktor, der alle zuvor erwähnten Dienstschnittstellen erfordert (also IExampleTransientService, IExampleScopedService und IExampleSingletonService). Das Objekt macht eine einzelne Methode verfügbar, die es dem Consumer ermöglicht, dem Dienst mit einem bestimmten lifetimeDetails-Parameter Informationen zu übermitteln. Wenn die ReportServiceLifetimeDetails-Methode abgerufen wird, protokolliert sie den eindeutigen Bezeichner jedes Diensts mit der Dienstlebensdauermeldung. Die Protokollmeldungen helfen beim Visualisieren der Dienstlebensdauer.

Registrieren von Diensten für die DI

Aktualisieren Sie Program.cs mit dem folgenden Code:

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

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddTransient<IExampleTransientService, ExampleTransientService>();
builder.Services.AddScoped<IExampleScopedService, ExampleScopedService>();
builder.Services.AddSingleton<IExampleSingletonService, ExampleSingletonService>();
builder.Services.AddTransient<ServiceLifetimeReporter>();

using IHost host = builder.Build();

ExemplifyServiceLifetime(host.Services, "Lifetime 1");
ExemplifyServiceLifetime(host.Services, "Lifetime 2");

await host.RunAsync();

static void ExemplifyServiceLifetime(IServiceProvider hostProvider, string lifetime)
{
    using IServiceScope serviceScope = hostProvider.CreateScope();
    IServiceProvider provider = serviceScope.ServiceProvider;
    ServiceLifetimeReporter logger = provider.GetRequiredService<ServiceLifetimeReporter>();
    logger.ReportServiceLifetimeDetails(
        $"{lifetime}: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()");

    Console.WriteLine("...");

    logger = provider.GetRequiredService<ServiceLifetimeReporter>();
    logger.ReportServiceLifetimeDetails(
        $"{lifetime}: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()");

    Console.WriteLine();
}

Jede Erweiterungsmethode vom Typ services.Add{LIFETIME}<{SERVICE}> fügt Dienste hinzu (und konfiguriert diese möglicherweise). Es wird empfohlen, dass Apps dieser Konvention folgen. Platzieren Sie Erweiterungsmethoden nicht im Microsoft.Extensions.DependencyInjection-Namespace, es sei denn, Sie erstellen ein offizielles Microsoft-Paket. Erweiterungsmethoden, die im Microsoft.Extensions.DependencyInjection-Namespace definiert sind:

  • Werden in IntelliSense ohne zusätzliche using-Blöcke angezeigt.
  • Reduzieren Sie die Anzahl der erforderlichen using-Anweisungen in Program- oder Startup-Klassen, über die diese Erweiterungsmethoden in der Regel aufgerufen werden.

Die App erfüllt diese Funktionen:

Zusammenfassung

In dieser Beispiel-App haben Sie mehrere Schnittstellen und entsprechende Implementierungen erstellt. Jeder dieser Dienste wird eindeutig identifiziert und mit einer ServiceLifetime-Enumeration gekoppelt. Die Beispiel-App veranschaulicht das Registrieren von Dienstimplementierungen für eine Schnittstelle und das Registrieren von reinen Klassen ohne Sicherungsschnittstellen. Die Beispiel-App veranschaulicht dann, wie die als Konstruktorparameter definierten Abhängigkeiten zur Laufzeit aufgelöst werden.

Wenn Sie die App ausführen, wird eine Ausgabe wie die folgende angezeigt:

// Sample output:
// Lifetime 1: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: d08a27fa-87d2-4a06-98d7-2773af886125 (Always different)
//     IExampleScopedService: 402c83c9-b4ed-4be1-b78c-86be1b1d908d (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// ...
// Lifetime 1: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: b43d68fb-2c7b-4a9b-8f02-fc507c164326 (Always different)
//     IExampleScopedService: 402c83c9-b4ed-4be1-b78c-86be1b1d908d (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// 
// Lifetime 2: Call 1 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: f3856b59-ab3f-4bbd-876f-7bab0013d392 (Always different)
//     IExampleScopedService: bba80089-1157-4041-936d-e96d81dd9d1c (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)
// ...
// Lifetime 2: Call 2 to provider.GetRequiredService<ServiceLifetimeReporter>()
//     IExampleTransientService: a8015c6a-08cd-4799-9ec3-2f2af9cbbfd2 (Always different)
//     IExampleScopedService: bba80089-1157-4041-936d-e96d81dd9d1c (Changes only with lifetime)
//     IExampleSingletonService: a61f1ff4-0b14-4508-bd41-21d852484a7b (Always the same)

Anhand der App-Ausgabe erkennen Sie Folgendes:

  • Transient-Dienste unterscheiden sich immer, und mit jedem erneuten Abruf des Diensts wird eine neue Instanz erstellt.
  • Scoped-Dienste ändern sich nur durch einen neuen Bereich, Instanzen sind jedoch innerhalb eines Bereichs immer gleich.
  • Singleton-Dienste sind immer gleich, und eine neue Instanz wird nur einmal erstellt.

Siehe auch