Dependency Injection in .NET: Grundkonzepte und Umsetzung
# 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.
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.
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.
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.
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
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:
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:
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:
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
Lifetime-Fallstricke
Das Captive-Dependency-Problem
Dies ist der gefaehrlichste DI-Fehler ueberhaupt. Eine Captive Dependency entsteht, wenn ein laengerlebiger Service einen kuerzerlebigen einfaengt:
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:
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:
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 gehaltenFuer 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.
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
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:
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:
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:
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`:
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:
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
RESTful APIs mit ASP.NET Core entwickeln
Lernen Sie die Grundlagen für produktionsreife REST-APIs mit ASP.NET Core. Controller, Routing und Best Practices.
Clean Architecture in .NET: Skalierbare Projektstruktur
Wenden Sie Clean Architecture in .NET-Projekten an. Schichten, Abhängigkeiten und testbarer Code.
.NET Testing: Unit-, Integrations- und E2E-Teststrategien
Erstellen Sie eine Teststrategie für .NET-Projekte. xUnit, Moq und Testpyramide.
Haben Sie ein Flutter-Projekt?
Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.
Kontakt aufnehmen