S

super-dev — full-stack

@super-dev.app · Full-stack · ★ offen für Aufträge
12,8k Abonnenten·47 Videos·Aktiv seit 2017

Full-Stack-Entwickler .NET / Angular mit DevOps-Erfahrung auf Azure Cloud. Auch zu Hause mit Flutter + Firebase. Ich entwickle Produkte, deploye sie und überwache sie.

Zurück zu den Artikeln
.NET
.NET

CQRS und Vertical Slices ohne Overengineering

Artikel 3 von 5 — Modernes .NET
Minimal APIs, CQRS, gRPC, Source Generators: sauberes und testbares .NET 8, ohne unnötigen Overhead.

Sobald jemand CQRS ausspricht, fahren viele Teams schweres Geschütz auf: Event Sourcing, einen Message Bus, zwei Datenbanken. Dabei ist CQRS zunächst eine bescheidene Idee — Lese- und Schreibzugriffe trennen — die man ganz ohne Overhead anwenden kann, indem man den Code in Vertical Slices organisiert.

Nach Funktionalität schneiden, nicht nach Schicht

Die Schichtenarchitektur zerlegt eine Funktionalität auf fünf Ordner: Controllers , Services , Repositories , DTOs , Validators . Um „eine Bestellung erstellen" zu verstehen, springt man von Datei zu Datei. Der Vertical Slice kehrt die Logik um: ein Ordner pro Funktionalität, alles Zugehörige an einem Ort.

Bash
1Features/
2 Orders/
3 CreateOrder.cs # commande + handler + validateur
4 GetOrderById.cs # requête + handler
5 ListOrders.cs

Jeder Slice ist eigenständig. Man liest ihn von oben nach unten, löscht ihn ohne Seiteneffekte, und zwei Slices teilen nur die Domäne — niemals einen Allzweck-„Service".

Command und Query, zwei unterschiedliche Absichten

Ein Command ändert den Zustand und gibt (idealerweise) nur eine ID zurück. Eine Query liest nur das, was die Ansicht benötigt, und umgeht dabei oft die Domäne, um direkt auf ein DTO zu projizieren. Sie getrennt zu modellieren, verdeutlicht die Absicht:

C#
1public sealed record CreateOrder(Guid CustomerId, IReadOnlyList<LineItem> Items)
2 : IRequest<Guid>;
3
4public sealed class CreateOrderHandler(AppDbContext db)
5 : IRequestHandler<CreateOrder, Guid>
6{
7 public async Task<Guid> Handle(CreateOrder command, CancellationToken ct)
8 {
9 var order = Order.Create(command.CustomerId, command.Items);
10 db.Orders.Add(order);
11 await db.SaveChangesAsync(ct);
12
13 return order.Id;
14 }
15}

Der Handler bleibt schlank : Er orchestriert, er denkt nicht. Die Geschäftslogik lebt in Order.Create , nicht im Handler — sonst hat man den „Service" nur in eine andere Datei verschoben.

Der Mediator, optional

Man sieht CQRS häufig zusammen mit MediatR . Der Mediator entkoppelt den Endpoint vom Handler und bietet einen Einstiegspunkt für Pipeline Behaviors (Validierung, Logging, Transaktion). Das ist praktisch, aber es ist nicht CQRS: Man kann den Handler auch direkt injizieren.

C#
1group.MapPost("/", async (CreateOrder command, ISender sender) =>
2{
3 var id = await sender.Send(command);
4
5 return TypedResults.Created($"/orders/{id}", new { id });
6});

Bei einer kleinen Anwendung ist es völlig legitim, den Mediator wegzulassen und den Handler direkt aufzurufen — weniger Indirektion, weniger Magie.

Nicht überentwerfen

Die Frage, die man sich bei jedem Slice stellen sollte: Brauche ich das wirklich? Getrennte Datenbanken, asynchrone Projektionen, Event Sourcing — das sind Antworten auf konkrete Skalierungsprobleme (massiv mehr Lesezugriffe als Schreibzugriffe, unveränderliches Audit-Log). Ohne dieses Problem fügen sie nur Latenz und Konsistenzfehler hinzu. Gutes CQRS bedeutet in 90 % der Fälle: getrennte Commands und Queries, ein einziger DbContext , lesbare Slices.

CQRS ist keine Architektur, sondern eine Benennungs-Disziplin . Trennt die Absichten, haltet die Handler schlank, und fügt einen Message Bus erst dann hinzu, wenn eine Metrik euch dazu zwingt.
super-dev — portfolio.app
// Weiter in .NET
{ }
Einen .NET 8-Monolithen stranglieren, ohne alles zu zerstören
9 min • 3,2k Aufrufe
ƒ()
Minimal APIs + EF Core: eine saubere .NET 8 API
8 min • 3,1k Aufrufe
gRPC zwischen .NET-Microservices
9 min • 1,8k Aufrufe