Dependency Injection in .NET: Grundkonzepte und Umsetzung

14 Min. Lesezeit9. Februar 2026Aktualisiert: 9. März 2026
Dependency Injection .NETDI .NET Core.NET service lifetimesTransient Scoped Singleton.NET IoC containerConstructor injection C#.NET best practicesLoose coupling .NET

# Dependency Injection in .NET: Grundlagen und Umsetzung

Dependency Injection (DI) ist ein zentrales Architekturprinzip in ASP.NET Core. Es handelt sich nicht um ein nachtraeglich aufgesetztes Feature, sondern um ein Konzept, das tief im Framework verankert ist -- von Middleware ueber Controller bis hin zu Razor Pages. Richtig eingesetzt reduziert DI die Kopplung zwischen Modulen, verbessert die Testbarkeit und erleichtert die Weiterentwicklung. Falsch eingesetzt fuehrt es zu subtilen Fehlern, die erst unter Last oder in der Produktion sichtbar werden.

In grossen .NET-Codebasen habe ich festgestellt, dass die Qualitaet des DI-Setups oft ein zuverlaessiger Indikator fuer die allgemeine Codegesundheit ist. Teams, die sich Zeit nehmen, Lifetimes, Registrierungsstrategien und das Composition-Root-Konzept zu verstehen, produzieren Systeme, die deutlich einfacher zu warten und zu erweitern sind.

Service Lifetimes verstehen

Der eingebaute Container in ASP.NET Core unterstuetzt drei Lebensdauern. Die falsche Wahl ist eine der haeufigsten Fehlerquellen in .NET-Anwendungen.

Transient

Bei jeder Aufloesung wird eine neue Instanz erzeugt. Dies ist die sicherste Standardwahl fuer leichtgewichtige, zustandslose Services.

csharp
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();

Verwenden Sie Transient, wenn der Service keinen gemeinsamen Zustand haelt und die Erzeugung guenstig ist. Oeffnet der Service eine Datenbankverbindung oder allokiert er erheblichen Speicher, kann Transient verschwenderisch sein.

Scoped

Eine Instanz pro Request-Scope. In ASP.NET Core bedeutet das eine Instanz pro HTTP-Request. Dies ist die Standardwahl fuer Services, die mit request-bezogenen Daten arbeiten.

csharp
builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>();
builder.Services.AddScoped<IUnitOfWork, EfUnitOfWork>();

Entity Frameworks `DbContext` wird standardmaessig als Scoped registriert, und das hat einen guten Grund: Er verfolgt Entity-Aenderungen innerhalb einer einzelnen Arbeitseinheit (Unit of Work). Eine gemeinsame Nutzung ueber Requests hinweg wuerde Concurrency-Probleme und veraltete Daten verursachen.

Singleton

Eine einzige Instanz fuer die gesamte Anwendungslebensdauer. Die Instanz wird bei der ersten Aufloesung erstellt und fuer jeden weiteren Request wiederverwendet.

csharp
builder.Services.AddSingleton<ICacheService, InMemoryCacheService>();
builder.Services.AddSingleton<IConnectionMultiplexer>(sp =>
    ConnectionMultiplexer.Connect(configuration.GetConnectionString(class="code-string">"Redis")!));

Singletons muessen thread-sicher sein. Sie duerfen nicht von Scoped- oder Transient-Services abhaengen -- andernfalls entsteht das weiter unten beschriebene Captive-Dependency-Problem.

Service-Registrierungsmuster

Grundlegende Interface-Registrierung

Das gaengigste und empfohlene Muster ist die Registrierung ueber ein Interface. Dies schafft eine klare Trennlinie zwischen Konsument und Implementierung.

csharp
builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddScoped<INotificationService, EmailNotificationService>();

Mehrere Implementierungen

Wenn Sie mehrere Implementierungen desselben Interfaces benoetigen, koennen Sie alle registrieren und `IEnumerable` injizieren:

csharp
builder.Services.AddScoped<INotificationService, EmailNotificationService>();
builder.Services.AddScoped<INotificationService, SmsNotificationService>();
builder.Services.AddScoped<INotificationService, PushNotificationService>();

class=class="code-string">"code-comment">// Im Konsumenten:
public class OrderProcessor
{
    private readonly IEnumerable<INotificationService> _notifiers;

    public OrderProcessor(IEnumerable<INotificationService> notifiers)
    {
        _notifiers = notifiers;
    }

    public async Task NotifyAll(Order order)
    {
        foreach (var notifier in _notifiers)
            await notifier.SendAsync(order.ToNotification());
    }
}

Factory-Registrierungen

Manchmal haengt die Objekterzeugung von Laufzeitentscheidungen ab. Factory-Delegates loesen das sauber:

csharp
builder.Services.AddScoped<IPaymentGateway>(sp =>
{
    var config = sp.GetRequiredService<IOptions<PaymentOptions>>().Value;
    return config.Provider switch
    {
        class="code-string">"Stripe" => new StripeGateway(config.ApiKey),
        class="code-string">"PayPal" => new PayPalGateway(config.ClientId, config.Secret),
        _ => throw new InvalidOperationException($class="code-string">"Unbekannter Zahlungsanbieter: {config.Provider}")
    };
});

Verwenden Sie Factory-Registrierungen nur, wenn die Objekterzeugung tatsaechlich Laufzeitverzweigung erfordert. Wenn Sie fuer die meisten Registrierungen Factories schreiben, deutet das auf ein tieferliegendes Designproblem hin.

Keyed Services (.NET 8+)

.NET 8 fuehrte Keyed Services ein, die das Problem mehrerer Implementierungen weitaus eleganter loesen als Factory-Workarounds:

csharp
builder.Services.AddKeyedScoped<IStorageService, AzureBlobStorage>(class="code-string">"azure");
builder.Services.AddKeyedScoped<IStorageService, S3Storage>(class="code-string">"aws");
builder.Services.AddKeyedScoped<IStorageService, LocalFileStorage>(class="code-string">"local");

class=class="code-string">"code-comment">// Im Konsumenten:
public class DocumentService
{
    private readonly IStorageService _storage;

    public DocumentService([FromKeyedServices(class="code-string">"azure")] IStorageService storage)
    {
        _storage = storage;
    }
}

Vor .NET 8 erforderte dieses Muster umstaendliche Workarounds mit Factory-Delegates oder Wrapper-Typen. In grossen .NET-Codebasen habe ich festgestellt, dass Keyed Services den Boilerplate-Code, den Teams frueher fuer Strategy-Patterns geschrieben haben, dramatisch reduzieren.

Options Pattern

Fuer konfigurationsintensive Services bietet das Options Pattern einen stark typisierten, validierbaren Ansatz:

csharp
class=class="code-string">"code-comment">// Options-Klasse definieren
public class SmtpOptions
{
    public const string SectionName = class="code-string">"Smtp";

    public string Host { get; set; } = string.Empty;
    public int Port { get; set; } = class="code-number">587;
    public string Username { get; set; } = string.Empty;
    public string Password { get; set; } = string.Empty;
    public bool UseSsl { get; set; } = true;
}

class=class="code-string">"code-comment">// Mit Validierung registrieren
builder.Services
    .AddOptions<SmtpOptions>()
    .BindConfiguration(SmtpOptions.SectionName)
    .ValidateDataAnnotations()
    .ValidateOnStart();

class=class="code-string">"code-comment">// Im Service injizieren
public class SmtpEmailSender : IEmailSender
{
    private readonly SmtpOptions _options;

    public SmtpEmailSender(IOptions<SmtpOptions> options)
    {
        _options = options.Value;
    }
}

Beachten Sie den Unterschied zwischen `IOptions`, `IOptionsSnapshot` und `IOptionsMonitor`. Ersteres ist ein Singleton, das die Konfiguration einmalig beim Start liest. `IOptionsSnapshot` ist Scoped und liest bei jedem Request neu. `IOptionsMonitor` ist Singleton, unterstuetzt aber Aenderungsbenachrichtigungen. Waehlen Sie die Variante, die zu Ihren Reload-Anforderungen passt.

Lifetime-Fallstricke

Das Captive-Dependency-Problem

Dies ist der gefaehrlichste DI-Fehler ueberhaupt. Eine Captive Dependency entsteht, wenn ein laengerlebiger Service einen kuerzerlebigen einfaengt:

csharp
class=class="code-string">"code-comment">// FALSCH: Singleton faengt einen Scoped-Service ein
builder.Services.AddSingleton<ICacheWarmingService, CacheWarmingService>();
builder.Services.AddScoped<IProductRepository, SqlProductRepository>();

public class CacheWarmingService : ICacheWarmingService
{
    private readonly IProductRepository _repo; class=class="code-string">"code-comment">// Dies wird EINGEFANGEN

    public CacheWarmingService(IProductRepository repo)
    {
        _repo = repo; class=class="code-string">"code-comment">// Diese Scoped-Instanz lebt so lange wie der Singleton
    }
}

Was passiert hier? Das `SqlProductRepository` war fuer die Dauer eines einzelnen Requests gedacht, aber der Singleton haelt dauerhaft eine Referenz. Verwendet das Repository einen `DbContext`, waechst dessen Change Tracker unbegrenzt, Verbindungen bleiben offen, und es entstehen Speicherlecks und veraltete Daten -- oft erst unter Produktionslast sichtbar.

In der Entwicklungsumgebung koennen Sie dies durch Scope-Validierung erkennen:

csharp
builder.Host.UseDefaultServiceProvider(options =>
{
    options.ValidateScopes = true;    class=class="code-string">"code-comment">// Wirft bei Captive Dependencies eine Exception
    options.ValidateOnBuild = true;   class=class="code-string">"code-comment">// Validiert alle Registrierungen beim Start
});

Aktivieren Sie beides in Entwicklungsumgebungen. Der geringe Startup-Overhead ist die verhinderten Fehler allemal wert.

Dispose von Transient-Services

Der DI-Container verfolgt und disposed `IDisposable`-Transient-Services. Das bedeutet, sie werden erst am Ende ihres Scopes durch den Garbage Collector freigegeben. Werden viele Disposable-Transients innerhalb eines Scopes aufgeloest, akkumuliert sich Speicher:

csharp
class=class="code-string">"code-comment">// Vorsicht: Jede aufgeloeste Instanz wird vom Container verfolgt
builder.Services.AddTransient<IDbConnection>(sp =>
    new SqlConnection(connectionString)); class=class="code-string">"code-comment">// Jede wird bis zum Scope-Dispose gehalten

Fuer Disposable-Transients sollten Sie die Lebensdauer selbst verwalten oder eine Factory verwenden, die nicht-verfolgte Instanzen zurueckgibt.

Service-Locator-Anti-Pattern

Das Service-Locator-Pattern bedeutet, Services in der Geschaeftslogik direkt ueber `GetService` oder `GetRequiredService` aufzuloesen. Dies ist ein Anti-Pattern, weil es Abhaengigkeiten verbirgt und den Code schwerer testbar und nachvollziehbar macht.

csharp
class=class="code-string">"code-comment">// SCHLECHT: Service Locator -- Abhaengigkeiten sind verborgen
public class OrderProcessor
{
    private readonly IServiceProvider _provider;

    public OrderProcessor(IServiceProvider provider)
    {
        _provider = provider;
    }

    public void Process(Order order)
    {
        var repo = _provider.GetRequiredService<IOrderRepository>();
        var mailer = _provider.GetRequiredService<IEmailSender>();
        class=class="code-string">"code-comment">// ...
    }
}

class=class="code-string">"code-comment">// GUT: Constructor Injection -- Abhaengigkeiten sind explizit
public class OrderProcessor
{
    private readonly IOrderRepository _repo;
    private readonly IEmailSender _mailer;

    public OrderProcessor(IOrderRepository repo, IEmailSender mailer)
    {
        _repo = repo;
        _mailer = mailer;
    }

    public void Process(Order order)
    {
        class=class="code-string">"code-comment">// _repo und _mailer direkt verwenden
    }
}

Die Constructor-Injection-Variante zeigt sofort, was die Klasse benoetigt. Die Service-Locator-Variante versteckt diese Beduerfnisse hinter `IServiceProvider`, sodass die tatsaechlichen Abhaengigkeiten nur durch Lesen der gesamten Implementierung erkennbar sind.

Es gibt berechtigte Verwendungen von `IServiceProvider` -- in Factory-Registrierungen, in Middleware oder beim manuellen Erstellen von Scopes fuer Hintergrundaufgaben. In Domain- und Application-Services hat es jedoch nichts zu suchen.

In grossen .NET-Codebasen habe ich festgestellt, dass sich die Verwendung des Service Locators ausbreitet, sobald er einmal eingefuehrt wurde. Ein Entwickler greift als Abkuerzung darauf zurueck, und andere folgen. Fruehes Erkennen in Code Reviews spart spaeter erheblichen Refactoring-Aufwand.

Testen mit DI

Einer der groessten Vorteile sauberer DI ist die Testbarkeit. Wenn jede Abhaengigkeit ueber den Konstruktor injiziert wird, ist das Austauschen realer Implementierungen gegen Test-Doubles trivial.

Unit Tests mit Mocks

csharp
public class OrderServiceTests
{
    [Fact]
    public async Task PlaceOrder_MitGueltigerWare_SendetBestaetigung()
    {
        class=class="code-string">"code-comment">// Arrange
        var mockRepo = new Mock<IOrderRepository>();
        mockRepo.Setup(r => r.SaveAsync(It.IsAny<Order>()))
                .ReturnsAsync(true);

        var mockMailer = new Mock<IEmailSender>();

        var service = new OrderService(mockRepo.Object, mockMailer.Object);

        class=class="code-string">"code-comment">// Act
        await service.PlaceOrderAsync(new Order { Items = testItems });

        class=class="code-string">"code-comment">// Assert
        mockMailer.Verify(m => m.SendAsync(
            It.Is<EmailMessage>(e => e.Subject.Contains(class="code-string">"Bestaetigung"))),
            Times.Once);
    }
}

Integrationstests mit WebApplicationFactory

Die `WebApplicationFactory` von ASP.NET Core erlaubt es, reale Services in Integrationstests durch Test-Implementierungen zu ersetzen:

csharp
public class OrdersApiTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public OrdersApiTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                class=class="code-string">"code-comment">// Den echten Datenbank-Context entfernen
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
                if (descriptor != null)
                    services.Remove(descriptor);

                class=class="code-string">"code-comment">// In-Memory-Datenbank fuer Tests hinzufuegen
                services.AddDbContext<AppDbContext>(options =>
                    options.UseInMemoryDatabase(class="code-string">"TestDb"));

                class=class="code-string">"code-comment">// E-Mail-Sender durch Fake-Implementierung ersetzen
                services.AddScoped<IEmailSender, FakeEmailSender>();
            });
        });
    }

    [Fact]
    public async Task PostOrder_GibtCreatedZurueck()
    {
        var client = _factory.CreateClient();

        var response = await client.PostAsJsonAsync(class="code-string">"/api/orders", new
        {
            Items = new[] { new { ProductId = class="code-number">1, Quantity = class="code-number">2 } }
        });

        Assert.Equal(HttpStatusCode.Created, response.StatusCode);
    }
}

Dieser Ansatz ermoeglicht vollstaendige HTTP-Pipeline-Tests, ohne Ihre Produktionsdatenbank zu beruehren oder echte E-Mails zu versenden. Der DI-Container macht diesen Austausch sauber und vorhersagbar.

Haeufige DI-Fehler

1. Alles registrieren

Nicht jede Klasse gehoert in den DI-Container. Einfache Datenobjekte, Werttypen, Extension Methods und interne Hilfsklassen haben dort nichts verloren. Uebermaessiges Registrieren macht das Composition Root unuebersichtlich und erschwert das Verstaendnis des tatsaechlichen Abhaengigkeitsgraphen.

2. Konstruktor-Ueberinjektion

Wenn ein Konstruktor mehr als vier oder fuenf Abhaengigkeiten entgegennimmt, verletzt die Klasse wahrscheinlich das Single-Responsibility-Prinzip. DI macht dies sichtbar -- das ist ein Feature, kein Problem:

csharp
class=class="code-string">"code-comment">// Dies ist ein Code Smell, kein DI-Problem
public class MegaService(
    IOrderRepo orders,
    IProductRepo products,
    IUserRepo users,
    IEmailSender email,
    ILogger<MegaService> logger,
    IPaymentGateway payment,
    IInventoryService inventory,
    IShippingService shipping) { }

Refactoren Sie die Klasse in kleinere, fokussierte Services, anstatt die Komplexitaet zu verbergen.

3. Container-Abstraktionen nach aussen tragen

Vermeiden Sie es, Ihre Domain- oder Application-Schicht an `Microsoft.Extensions.DependencyInjection` zu koppeln. Ihre Geschaeftslogik sollte nichts ueber Container wissen:

csharp
class=class="code-string">"code-comment">// SCHLECHT: Domain-Service abhaengig von DI-Container-Typen
using Microsoft.Extensions.DependencyInjection;

public class PricingEngine
{
    [FromKeyedServices(class="code-string">"premium")]
    public IDiscountStrategy Discount { get; set; } class=class="code-string">"code-comment">// Nicht so machen
}

Halten Sie die DI-Verdrahtung im Composition Root -- typischerweise in `Program.cs` oder in Extension Methods auf `IServiceCollection`.

4. Asynchrone Initialisierung ignorieren

Manche Services benoetigen asynchrones Setup (Datenbankverbindung herstellen, Cache aufwaermen). Blockieren Sie den Konstruktor nicht mit `.Result` oder `.GetAwaiter().GetResult()`. Verwenden Sie stattdessen `IHostedService` oder `BackgroundService`:

csharp
public class CacheWarmupService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public CacheWarmupService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        using var scope = _scopeFactory.CreateScope();
        var cache = scope.ServiceProvider.GetRequiredService<ICacheService>();
        await cache.WarmAsync(stoppingToken);
    }
}

Beachten Sie die Verwendung von `IServiceScopeFactory` zum manuellen Erstellen eines Scopes -- dies ist der korrekte Weg, um aus einem Singleton-Background-Service auf Scoped-Services zuzugreifen.

Best Practices fuer das Composition Root

Halten Sie Ihre `Program.cs` uebersichtlich, indem Sie Registrierungen in Extension Methods gruppieren:

csharp
class=class="code-string">"code-comment">// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddPersistence(builder.Configuration)
    .AddApplicationServices()
    .AddInfrastructure(builder.Configuration);

class=class="code-string">"code-comment">// ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddPersistence(
        this IServiceCollection services, IConfiguration config)
    {
        services.AddDbContext<AppDbContext>(options =>
            options.UseSqlServer(config.GetConnectionString(class="code-string">"Default")));

        services.AddScoped<IOrderRepository, SqlOrderRepository>();
        services.AddScoped<IProductRepository, SqlProductRepository>();

        return services;
    }

    public static IServiceCollection AddApplicationServices(
        this IServiceCollection services)
    {
        services.AddScoped<IOrderService, OrderService>();
        services.AddScoped<IPricingEngine, PricingEngine>();

        return services;
    }
}

Das haelt das Composition Root auch bei wachsender Anwendung lesbar. In grossen .NET-Codebasen habe ich festgestellt, dass Teams, die dieses Muster frueh uebernehmen, das beruehmte "500-Zeilen-Program.cs"-Problem vermeiden, unter dem viele Projekte leiden.

Fazit

Gutes DI ist mehr als Container-Konfiguration -- es ist bewusstes Abhaengigkeitsdesign. Der eingebaute Container in ASP.NET Core ist fuer die meisten Anwendungen leistungsfaehig genug, und das Verstaendnis seiner Lifetimes, Registrierungsmuster und Einschraenkungen bewahrt Sie vor subtilen, schwer diagnostizierbaren Fehlern. Dieses Design bestimmt unmittelbar die Wartbarkeit, Testbarkeit und Aenderungssicherheit Ihres gesamten Systems.

Gerne analysiere ich Ihre DI-Struktur auf Lifetime- und Kopplungsrisiken -- sprechen Sie mich einfach an.

Verwandte Artikel

Haben Sie ein Flutter-Projekt?

Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.

Kontakt aufnehmen