Wenn dir das Muster aus Lektion 2 bekannt vorkam, liegt das daran, dass .NET an mehreren prominenten Stellen darauf gebaut ist.
Stream — das Lehrbuchbeispiel im BCL
Stream ist seit .NET 1.0 eine Decorator-Hierarchie. GZipStream, CryptoStream und BufferedStream sind alle Streams, und alle enthalten einen Stream:
using var datei = File.Create("export.gz");
using var komprimiert = new GZipStream(datei, CompressionLevel.Optimal);
using var schreiber = new StreamWriter(komprimiert);
await schreiber.WriteLineAsync("artikelnr;preis");Der StreamWriter weiss nichts von Komprimierung, der GZipStream nichts von Dateien. Verschlüsselung schiebst du dazwischen, indem du eine Zeile einfügst — kein Aufrufer ändert sich.
ASP.NET-Core-Middleware
Die Request-Pipeline ist dasselbe Prinzip, nur mit Delegates statt Schnittstellen. Jede Middleware bekommt next — die nächste Schicht nach innen:
app.UseExceptionHandler("/fehler"); // aussen: fängt alles darunter
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization(); // innen: schützt die Endpunkte
app.MapControllers();Dieselbe Reihenfolgefrage, mit härteren Folgen
UseAuthorization vor UseAuthentication bedeutet, dass die Autorisierung noch keinen angemeldeten Benutzer kennt — jede Anfrage wird abgewiesen. Die Reihenfolge der Use…-Aufrufe ist die Schachtelung der Decorators.
DelegatingHandler bei HttpClient
Für ausgehende Anfragen gilt dasselbe. Ein DelegatingHandler ist ein Decorator um den inneren Handler — genau dort gehören Wiederholungen, Authentifizierungs-Header und Korrelations-IDs hin:
public sealed class KorrelationsHandler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken token)
{
request.Headers.Add("X-Correlation-Id", Activity.Current?.Id ?? Guid.NewGuid().ToString());
return base.SendAsync(request, token); // nach innen weiterreichen
}
}
builder.Services.AddHttpClient("zahlungen")
.AddHttpMessageHandler<KorrelationsHandler>();Dekorieren im DI-Container
Der eingebaute Container von .NET kennt keinen Decorate-Aufruf. Von Hand geht es trotzdem — über eine Factory-Registrierung:
builder.Services.AddSingleton<PreisRepository>(); // der Kern, konkret
builder.Services.AddSingleton<IPreisRepository>(sp =>
new ProtokolliertesPreisRepository(
new GecachtesPreisRepository(
sp.GetRequiredService<PreisRepository>(),
sp.GetRequiredService<IMemoryCache>()),
sp.GetRequiredService<ILogger<ProtokolliertesPreisRepository>>()));Das ist ausführlich, aber es steht an genau einer Stelle und die Schachtelung ist beim Lesen sichtbar. Wer es kürzer mag, nimmt Scrutor:
builder.Services.AddSingleton<IPreisRepository, PreisRepository>();
builder.Services.Decorate<IPreisRepository, GecachtesPreisRepository>();
builder.Services.Decorate<IPreisRepository, ProtokolliertesPreisRepository>();
// zuletzt dekoriert = am weitesten aussenWann Decorator, wann nicht
Dafür: das Anliegen ist quer über viele Implementierungen dasselbe (Caching, Protokoll, Wiederholung, Messung, Autorisierung) und lässt sich vollständig aus der Kernklasse heraushalten.
Dagegen: der Decorator müsste das Ergebnis der inneren Schicht inhaltlich verstehen, oder er braucht Methoden, die die Schnittstelle nicht hat. Dann gehört die Logik in die Klasse — oder die Schnittstelle ist falsch geschnitten.
Der Preis
Ein Stack-Trace aus vier Schichten ist vier Rahmen länger, und beim Debuggen springst du durch Klassen, die alle fast dasselbe tun. Wenn eine Antwort falsch ist, musst du wissen, welche Schicht sie erzeugt hat.
Deshalb: Dekoriere wenige, klar benannte Anliegen. Fünf Schichten um ein Repository sind kein Zeichen von Sauberkeit, sondern eine Fehlersuche, die eine halbe Stunde länger dauert.
Try it: Task 1
Zwei Schichten um dieselbe Schnittstelle — und die Reihenfolge muss stimmen.