Was macht Dependency-Injection-Patterns?
Wann dieser Skill verwendet werden sollte
Verwenden Sie diesen Skill, wenn:
- Sie Service-Registrierungen in ASP.NET Core-Anwendungen organisieren möchten
- Sie übermäßig große Program.cs/Startup.cs-Dateien mit hunderten von Registrierungen vermeiden wollen
- Sie die Service-Konfiguration zwischen Produktion und Tests wiederverwendbar machen möchten
- Sie Bibliotheken entwerfen, die in Microsoft.Extensions.DependencyInjection integriert werden sollen
Referenzdatei
- advanced-patterns.md: Testen mit DI-Erweiterungen, Akka.NET-Actor-Scope-Management, bedingte/factory-/keyed-Registrierungsmuster
Das Problem
Ohne Organisation wird Program.cs unüberschaubar:
// SCHLECHT: Über 200 Zeilen unorganisierter Registrierungen
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IProductRepository, ProductRepository>();
builder.Services.AddScoped<IUserService, UserService>();
// ... 150 weitere Zeilen ...
Probleme: Zusammengehörige Registrierungen sind schwer zu finden, keine klaren Grenzen, keine Wiederverwendung in Tests, Merge-Konflikte.
Die Lösung: Erweiterungsmethoden-Komposition
Gruppieren Sie zusammengehörige Registrierungen in Erweiterungsmethoden:
// GUT: Saubere, zusammensetzbare Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddUserServices()
.AddOrderServices()
.AddEmailServices()
.AddPaymentServices()
.AddValidators();
var app = builder.Build();
Das Erweiterungsmethoden-Muster
Grundlegende Struktur
namespace MyApp.Users;
public static class UserServiceCollectionExtensions
{
public static IServiceCollection AddUserServices(this IServiceCollection services)
{
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IUserReadStore, UserReadStore>();
services.AddScoped<IUserWriteStore, UserWriteStore>();
services.AddScoped<IUserService, UserService>();
services.AddScoped<IUserValidationService, UserValidationService>();
return services;
}
}
Mit Konfiguration
namespace MyApp.Email;
public static class EmailServiceCollectionExtensions
{
public static IServiceCollection AddEmailServices(
this IServiceCollection services,
string configSectionName = "EmailSettings")
{
services.AddOptions<EmailOptions>()
.BindConfiguration(configSectionName)
.ValidateDataAnnotations()
.ValidateOnStart();
services.AddSingleton<IMjmlTemplateRenderer, MjmlTemplateRenderer>();
services.AddSingleton<IEmailLinkGenerator, EmailLinkGenerator>();
services.AddScoped<IUserEmailComposer, UserEmailComposer>();
services.AddScoped<IEmailSender, SmtpEmailSender>();
return services;
}
}
Dateiorganisation
Platzieren Sie Erweiterungsmethoden in der Nähe der Dienste, die sie registrieren:
src/
MyApp.Api/
Program.cs # Setzt alle Add*-Methoden zusammen
MyApp.Users/
Services/
UserService.cs
UserServiceCollectionExtensions.cs # AddUserServices()
MyApp.Orders/
OrderServiceCollectionExtensions.cs # AddOrderServices()
MyApp.Email/
EmailServiceCollectionExtensions.cs # AddEmailServices()
Konvention: {Feature}ServiceCollectionExtensions.cs neben den Diensten des Features.
Namenskonventionen
| Muster | Verwendungszweck |
|---|---|
Add{Feature}Services() | Allgemeine Feature-Registrierung |
Add{Feature}() | Kurzform, wenn eindeutig |
Configure{Feature}() | Wenn hauptsächlich Optionen gesetzt werden |
Use{Feature}() | Middleware (auf IApplicationBuilder) |
Vorteile für Tests
Das Add*-Muster ermöglicht es Ihnen, die Produktionskonfiguration in Tests wiederzuverwenden und nur das zu überschreiben, was anders ist. Funktioniert mit WebApplicationFactory, Akka.Hosting.TestKit und eigenständigen ServiceCollection.
Vollständige Testbeispiele finden Sie in advanced-patterns.md.
Geschichtete Erweiterungen
Bei größeren Anwendungen setzen Sie Erweiterungen hierarchisch zusammen:
public static class AppServiceCollectionExtensions
{
public static IServiceCollection AddAppServices(this IServiceCollection services)
{
return services
.AddDomainServices()
.AddInfrastructureServices()
.AddApiServices();
}
}
public static class DomainServiceCollectionExtensions
{
public static IServiceCollection AddDomainServices(this IServiceCollection services)
{
return services
.AddUserServices()
.AddOrderServices()
.AddProductServices();
}
}
Integration in Akka.Hosting
Das gleiche Muster funktioniert für die Akka.NET-Actor-Konfiguration:
public static class OrderActorExtensions
{
public static AkkaConfigurationBuilder AddOrderActors(
this AkkaConfigurationBuilder builder)
{
return builder
.WithActors((system, registry, resolver) =>
{
var orderProps = resolver.Props<OrderActor>();
var orderRef = system.ActorOf(orderProps, "orders");
registry.Register<OrderActor>(orderRef);
});
}
}
// Verwendung in Program.cs
builder.Services.AddAkka("MySystem", (builder, sp) =>
{
builder
.AddOrderActors()
.AddInventoryActors()
.AddNotificationActors();
});
Vollständige Akka.Hosting-Muster finden Sie im Skill akka-hosting-actor-patterns.
Anti-Patterns
Nicht: Alles in Program.cs registrieren
// SCHLECHT: Riesige Program.cs mit über 200 Zeilen Registrierungen
Nicht: Zu allgemeine Erweiterungen erstellen
// SCHLECHT: Zu vage, kommuniziert nicht, was registriert wird
public static IServiceCollection AddServices(this IServiceCollection services) { ... }
Nicht: Wichtige Konfiguration verstecken
// SCHLECHT: Versteckte Einstellungen
public static IServiceCollection AddDatabase(this IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer("hartcodierte-verbindungszeichenfolge")); // Versteckt!
}
// GUT: Konfiguration explizit akzeptieren
public static IServiceCollection AddDatabase(
this IServiceCollection services,
string connectionString)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
}
Zusammenfassung der Best Practices
| Praxis | Nutzen |
|---|---|
Gruppieren zusammengehöriger Dienste in Add*-Methoden | Saubere Program.cs, klare Grenzen |
| Platzieren von Erweiterungen nahe der registrierten Dienste | Leicht zu finden und zu warten |
Rückgabe von IServiceCollection für Methodenverkettung | Flüssige API |
| Akzeptieren von Konfigurationsparametern | Flexibilität |
Konsistente Namensgebung (Add{Feature}Services) | Auffindbarkeit |
| Testen durch Wiederverwendung von Produktionserweiterungen | Vertrauen, weniger Duplikate |
Lebensdauerverwaltung
| Lebensdauer | Verwenden, wenn | Beispiele |
|---|---|---|
| Singleton | Zustandslos, thread-sicher, teuer zu erstellen | Konfiguration, HttpClient-Fabriken, Caches |
| Scoped | Zustandsbehaftet pro Anfrage, Datenbankkontexte | DbContext, Repositories, Benutzerkontext |
| Transient | Leichtgewichtig, zustandsbehaftet, günstig zu erstellen | Validatoren, kurzlebige Helfer |
// SINGLETON: Zustandslose Dienste, sicher geteilt
services.AddSingleton<IMjmlTemplateRenderer, MjmlTemplateRenderer>();
// SCOPED: Datenbankzugriff, Zustand pro Anfrage
services.AddScoped<IUserRepository, UserRepository>();
// TRANSIENT: Günstig, kurzlebig
services.AddTransient<CreateUserRequestValidator>();
Scoped-Dienste benötigen einen Scope. ASP.NET Core erstellt einen pro HTTP-Anfrage. In Hintergrunddiensten und Akteuren erstellen Sie Scopes manuell.
Scope-Management-Muster für Akteure finden Sie in advanced-patterns.md.
Häufige Fehler
Injizieren von Scoped-Diensten in Singletons
// SCHLECHT: Singleton erfasst Scoped-Dienst - veralteter DbContext!
public class CacheService // Als Singleton registriert
{
private readonly IUserRepository _repo; // Scoped - beim Start erfasst!
}
// GUT: IServiceProvider injizieren, pro Operation einen Scope erstellen
public class CacheService
{
private readonly IServiceProvider _serviceProvider;
public async Task<User> GetUserAsync(string id)
{
using var scope = _serviceProvider.CreateScope();
var repo = scope.ServiceProvider.GetRequiredService<IUserRepository>();
return await repo.GetByIdAsync(id);
}
}
Kein Scope in Hintergrundaufgaben
// SCHLECHT: Kein Scope für Scoped-Dienste
public class BadBackgroundService : BackgroundService
{
private readonly IOrderService _orderService; // Scoped - wird einen Fehler auslösen!
}
// GUT: Für jede Arbeitseinheit einen Scope erstellen
public class GoodBackgroundService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
protected override async Task ExecuteAsync(CancellationToken ct)
{
using var scope = _scopeFactory.CreateScope();
var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();
// ...
}
}
Ressourcen
- Microsoft.Extensions.DependencyInjection: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection
- Akka.Hosting: https://github.com/akkadotnet/Akka.Hosting
- Akka.DependencyInjection: https://getakka.net/articles/actors/dependency-injection.html
- Options Pattern: Siehe Skill
microsoft-extensions-configuration