~/goldbarth / decisions $ ls

Minimal API ohne MediatR

Zwei Fragen prägen den API Host in den meisten .NET-Projekten: welches Routing-Modell und ob eine Mediator-Library verwendet werden soll. ServiceDeskLite beantwortet beide bewusst: Minimal API, und kein Mediator.

Beides ist in vielen Architekturen nicht die Standardwahl. MediatR im Besonderen ist zu einer Art Konvention geworden — früh hinzugefügt, überall verwendet, unabhängig davon, ob die Pipeline, die es ermöglicht, tatsächlich benötigt wird. Die Frage, mit der ich angefangen habe: Was fehlt, wenn ich es nicht hinzufüge?

Was MediatR hinzufügen würde

MediatRs Kernwert ist IPipelineBehavior<,>. Cross-cutting Concerns — jeden Handler-Aufruf loggen, Validation vor jedem Command ausführen, Authentication durchsetzen — können einmal als Behaviour ausgedrückt und auf jeden Handler-Aufruf angewendet werden, ohne die Call-Site zu berühren.

Ohne MediatR leben diese Concerns auf Middleware-Ebene oder werden per Handler behandelt. Für ServiceDeskLites aktuelle Oberfläche — vier Ticket-Endpoints, ein Aggregate — gibt es keine Cross-cutting Handler-Concerns, die eine Pipeline brauchen. Logging wird von Serilog auf Middleware-Ebene behandelt. Validation ist per Handler. Authentication ist nicht im Scope.

MediatR hinzuzufügen, bevor es etwas gibt, das in die Pipeline kommt, ist Infrastructure-Schuld, kein Kapital. Der Registration Layer, die ISender-Abstraktion, das Assembly-Scanning — alles davon ist Overhead für null aktuellen Nutzen.

Wie Handler stattdessen verdrahtet werden

Endpoints werden als statische Methoden auf einer RouteGroupBuilder-Extension-Klasse definiert. Handler-Typen werden direkt als Endpoint-Parameter durch das DI-aware Parameter-Binding des Frameworks injiziert:

group.MapPost("/", async (
    CreateTicketRequest request,
    CreateTicketHandler handler,
    CancellationToken ct) =>
{
    var command = request.ToCommand();
    var result = await handler.HandleAsync(command, ct);
    return result.ToHttpResult();
});

CreateTicketHandler ist als scoped Service in DI registriert. Der Endpoint ruft ihn direkt auf. Kein ISender.Send(command), kein Mediator-Lookup, keine Dispatch-Indirektion.

Die Call-Site ist explizit: Man kann einen Endpoint lesen und weiß genau, welchen Handler er aufruft. Es gibt keine Layer von convention-basiertem Dispatch, durch die man sich durcharbeiten muss.

Die CQRS-Frage

ServiceDeskLite trennt Commands und Queries als unterschiedliche Typen mit unterschiedlichen Handlers — CreateTicketHandler, GetTicketByIdHandler, UpdateTicketStatusHandler, GetTicketsHandler. Das ist leichtgewichtiges CQRS: separate Read- und Write-Models auf Application-Layer-Ebene, mit expliziten Handler-Typen pro Use Case.

Es ist nicht die schwergewichtigere CQRS-Variante — keine MediatR-Request-Pipeline, keine separate Read-Datenbank, keine Event-Projektion. Die Read/Write-Boundary ist ohne die Infrastructure sichtbar. Leichtgewichtiges CQRS mit direkter Handler-Injektion ist ein anderer Punkt auf der Trade-off-Kurve als „vollständiges CQRS mit MediatR”, und es lohnt sich, beides auseinanderzuhalten.

Wann überdenken

Es gibt zwei konkrete Auslöser. Erstens: ein Cross-cutting Concern, der gleichmäßig auf jeden Handler-Aufruf angewendet werden muss — Authentication-Enforcement, Retry-Logik, Distributed-Tracing-Spans. An diesem Punkt verdient IPipelineBehavior<,> seine Existenz und MediatR wird die richtige Antwort. Zweitens: die Endpoint-Oberfläche wächst so weit, dass jeder Endpoint mehrere Handler orchestriert, was die Parameter-Liste unhandlich macht. An diesem Punkt beginnen Controller oder ein Dispatcher, das Zeremoniell zu rechtfertigen.

Keines davon ist bisher passiert. Der Neubewertungspunkt ist explizit benannt, nicht auf unbestimmte Zeit verschoben — das ADR für diese Entscheidung listet die Auslöser auf. Wenn die Komplexität kommt, ist der Migrationspfad klar: Endpoints rufen ISender.Send(command) statt handler.HandleAsync(command, ct) auf. Die Handler selbst ändern sich nicht.

~/goldbarth/ decisions $ ls