Bevor du eine Factory schreibst, lohnt der Blick darauf, wo das Framework das Muster bereits einsetzt. Es sind mehr Stellen, als man denkt.
IHttpClientFactory
Das bekannteste Beispiel â und es existiert wegen eines echten Fehlers, den fast jeder einmal gemacht hat.
Ein HttpClient pro Aufruf zu erzeugen und zu entsorgen lĂ€sst die zugrunde liegenden Sockets im Zustand TIME_WAIT zurĂŒck; unter Last gehen dem Prozess die Ports aus. Ein einziger HttpClient als static wiederum merkt sich DNS-Auflösungen dauerhaft und schickt Anfragen nach einem Failover an die alte Adresse.
Die Factory verwaltet die Handler dazwischen und löst beides:
builder.Services.AddHttpClient("zahlungen", c =>
{
c.BaseAddress = new Uri("https://api.zahlungen.example");
c.Timeout = TimeSpan.FromSeconds(10);
});
// Im Service:
public sealed class ZahlungsClient
{
private readonly IHttpClientFactory _factory;
public ZahlungsClient(IHttpClientFactory factory) => _factory = factory;
public async Task<Beleg> HoleBelegAsync(string id)
{
var client = _factory.CreateClient("zahlungen");
return await client.GetFromJsonAsync<Beleg>($"belege/{id}")
?? throw new InvalidOperationException("Leere Antwort");
}
}Weitere Factories im Framework
ILoggerFactory (erzeugt Logger pro Kategorie), IServiceScopeFactory (erzeugt Scopes in Hintergrunddiensten), IDbContextFactory<T> (erzeugt DbContexts, wenn der Scope nicht passt â etwa in Blazor Server), ActivatorUtilities.CreateInstance (baut ein Objekt mit DI-AbhĂ€ngigkeiten plus eigenen Argumenten).
Der Scope-Fall in Hintergrunddiensten
Ein BackgroundService ist ein Singleton und lebt so lange wie die Anwendung. Ein DbContext ist Scoped und soll das ausdrĂŒcklich nicht. Reichst du den Context in den Hintergrunddienst hinein, hĂ€ltst du ihn fĂŒr die gesamte Laufzeit fest â das ist eine Captive Dependency, und sie fĂ€llt irgendwann als seltsamer Cache- oder Tracking-Fehler auf.
Die Antwort ist eine Factory fĂŒr Scopes:
public sealed class MahnlaufDienst : BackgroundService
{
private readonly IServiceScopeFactory _scopes;
public MahnlaufDienst(IServiceScopeFactory scopes) => _scopes = scopes;
protected override async Task ExecuteAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
using (var scope = _scopes.CreateScope()) // ein Scope pro Durchlauf
{
var db = scope.ServiceProvider.GetRequiredService<ShopContext>();
await VerarbeiteAsync(db, token);
}
await Task.Delay(TimeSpan.FromMinutes(15), token);
}
}
}Func<T> als Factory
Wenn eine Factory nur ein Objekt ohne Argumente erzeugt, brauchst du keine Klasse dafĂŒr â ein Delegate genĂŒgt und spart eine Schnittstelle:
builder.Services.AddTransient<Berichtsmappe>();
builder.Services.AddSingleton<Func<Berichtsmappe>>(sp =>
() => sp.GetRequiredService<Berichtsmappe>());
// Im Konsumenten â jeder Aufruf liefert eine frische Mappe:
public sealed class Berichtslauf(Func<Berichtsmappe> mappen)
{
public void Ausfuehren(IEnumerable<Abteilung> abteilungen)
{
foreach (var abteilung in abteilungen)
{
var mappe = mappen();
mappe.Fuelle(abteilung);
}
}
}Die Grenze zum Service Locator
Eine Factory, die IServiceProvider benutzt, ist in Ordnung â genau das ist ihre Aufgabe. Eine GeschĂ€ftsklasse, die IServiceProvider hereingereicht bekommt und sich nach Bedarf bedient, ist es nicht: Ihrer Signatur ist nicht mehr anzusehen, was sie braucht. Halte den Container in den Factories und in Program.cs.
Die Entscheidungshilfe
| Situation | Antwort |
|---|---|
| AbhÀngigkeit steht beim Start fest | Constructor Injection, keine Factory |
| Konkrete Klasse hÀngt von Laufzeitdaten ab | Simple Factory |
| Frisches Objekt pro Schleifendurchlauf | Func<T> oder IServiceScopeFactory |
| Fester Ablauf, ein austauschbarer Erzeugungsschritt | Factory Method |
| Mehrere Objekte mĂŒssen aus derselben Familie stammen | Abstract Factory |
Die erste Zeile deckt die meisten FÀlle ab. Das ist keine EnttÀuschung, sondern der Grund, warum DI-Container existieren.