Wenn gewöhnliches CRUD nicht mehr funktioniert: Einführung in CQRS und Event Sourcing
\n\nStellen Sie sich vor, Sie schreiben ein System für eine Bank. Jede Sekunde überprüfen Hunderte von Benutzern ihr Guthaben, überweisen Geld, eröffnen Einlagen. Ihre Anwendung in C# läuft auf einem standardmäßigen Entity Framework mit typischen CRUD-Operationen. Und plötzlich – die Datenbank stürzt ab. Warum? Weil ein schwerer Bericht eine Tabelle blockiert hat, während eine Transaktion geschrieben wurde. Kommt Ihnen bekannt vor?
Entwickler komplexer Systeme in C# stoßen früher oder später an die Grenzen der klassischen Architektur. Zwei leistungsstarke Muster kommen zur Hilfe: CQRS (Command Query Responsibility Segregation) und Event Sourcing. Lassen Sie uns sie in der Praxis durchgehen – mit lebendigen Beispielen, nicht mit trockener Theorie.
\n\nWas ist CQRS und warum ist es nicht nur "Trennung"?
\n\nCQRS geht nicht darum, Methoden in verschiedene Ordner zu legen. Es ist eine Philosophie: Befehle (ändern den Zustand) und Abfragen (lesen den Zustand) sollten getrennt leben. Im klassischen CRUD haben Sie ein Datenmodell sowohl zum Lesen als auch zum Schreiben. In CQRS gibt es mindestens zwei.
Das Problem des klassischen Ansatzes
\nAngenommen, Sie haben ein UserRepository mit einer Methode GetUserById. Wenn Sie einen Benutzer zur Anzeige auf dem Bildschirm lesen, benötigen Sie 3 Felder. Wenn Sie ihn aktualisieren, benötigen Sie 10 Felder und Prüfungen. Infolgedessen wird ein Modell zu einem Monster mit null-Feldern und verwirrenden Mappern.
In CQRS trennen Sie die Verantwortlichkeiten:
\n- \n
- Command – führt eine Aktion aus (z. B.
CreateOrderCommand) \n - Query – gibt Daten zurück (z. B.
GetOrderQuery) \n - Read Model – flaches DTO, zugeschnitten auf einen bestimmten Bildschirm \n
- Write Model – reichhaltiges Domänenmodell mit Geschäftslogik \n
CQRS in C# mit MediatR schreiben
\nDer beliebteste Weg, CQRS in C# zu implementieren, ist die Verwendung der Bibliothek MediatR. Sie implementiert das Mediator-Muster und passt perfekt zum Konzept von Befehlen und Abfragen.
Schritt 1: Definieren eines Befehls
\npublic record CreateOrderCommand(\n Guid CustomerId,\n List<OrderItemDto> Items,\n string ShippingAddress\n) : IRequest<Guid>; // Gibt die ID der erstellten Bestellung zurück\n\nSchritt 2: Befehlshandler
\npublic class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>\n{\n private readonly IOrderRepository _orderRepository;\n private readonly IUnitOfWork _unitOfWork;\n\n public CreateOrderCommandHandler(IOrderRepository orderRepository, IUnitOfWork unitOfWork)\n {\n _orderRepository = orderRepository;\n _unitOfWork = unitOfWork;\n }\n\n public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)\n {\n // Geschäftslogik: Prüfung, Validierung, Erstellung\n var order = new Order(request.CustomerId, request.ShippingAddress);\n \n foreach (var item in request.Items)\n {\n order.AddItem(item.ProductId, item.Quantity, item.Price);\n }\n\n await _orderRepository.AddAsync(order);\n await _unitOfWork.SaveChangesAsync(cancellationToken);\n \n return order.Id;\n }\n}\n\nSchritt 3: Definieren einer Abfrage
\npublic record GetOrderSummaryQuery(Guid OrderId) : IRequest<OrderSummaryDto>;\n\nSchritt 4: Abfragehandler (liest aus einer separaten Datenbank!)
\npublic class GetOrderSummaryQueryHandler : IRequestHandler<GetOrderSummaryQuery, OrderSummaryDto>\n{\n private readonly IReadDbContext _readDbContext; // Separater Kontext zum Lesen\n\n public GetOrderSummaryQueryHandler(IReadDbContext read