CQRS Handler (C#) mit Domänenlogik und Pipelines - erklärt über das Video von Derek Comartin
Command Query Responsibility Segregation (CQRS) ist ein leistungsfähiges Entwurfsmuster in .NET-Anwendungen, das dazu beiträgt, eine klare Trennung zwischen Lese- und Schreibvorgängen zu gewährleisten. Diese Trennung ermöglicht eine bessere Skalierbarkeit, Testbarkeit und Kontrolle über komplexe Geschäftslogik. Wenn Ihre Anwendung jedoch wächst, können Ihre CQRS-Handler in C# mit Logik aufgebläht werden, wodurch sie schwieriger zu warten und zu testen sind.
In seinem aufschlussreichen Video "Clean Up Bloated CQRS Handlers with Domain Logic & Pipelines" zeigt Derek Comartin, wie man solche CQRS-Befehlshandler aufräumt, indem man die Logik in Domänenmodelle verlagert und eine Pipeline strukturiert, um die Befehlslogik Schritt für Schritt zu verwalten. In diesem Artikel werden wir Dereks Ansatz im Detail untersuchen und lernen, wie man besser wartbare CQRS-Implementierungen in C# erstellen kann.
Das Problem mit aufgeblähten Handlern
Derek beginnt damit, dass er das übliche Szenario in vielen Webanwendungen hervorhebt: Sie öffnen einen CQRS-Handler in C#, und es ist ein Chaos. Es geht um Datenvalidierung, Autorisierung, Geschäftslogik, Zustandsübergänge, Ereignisveröffentlichung, Protokollierung - alles miteinander verwoben.
Er veranschaulicht dies anhand eines Befehlsobjekts für die Versendung einer Sendung. Der Bearbeiter ist verantwortlich für:
-
Zugriff auf den Datenspeicher zum Laden der Sendung.
-
Prüfen, ob der Versandstatus fertig ist.
-
Aktualisierung des Status (d. h. Aktualisierung der Daten).
-
Speichern von Änderungen in der Datenzugriffsschicht.
-
Senden Sie eine E-Mail.
- Veröffentlichung einer Domain-Veranstaltung.
All dies geschieht an einem einzigen Ort. Dies verstößt gegen die Absicht des CQRS-Musters, bei dem das Ziel darin besteht, Belange zu trennen und die Leistung und Wartbarkeit zu verbessern.
Verlagerung der Domänenlogik in das Datenmodell
Der erste Schritt von Derek besteht darin, die Validierungs- und Zustandsübergangslogik in das Datenmodell zu übertragen. Er erstellt eine Dispatch()-Methode innerhalb der Shipment-Klasse. Hier befindet sich nun die Fachlogik.
Anstatt den Versandstatus manuell im Handler zu überprüfen, wird die Logik in dieser Methode gekapselt, wodurch die Datenintegrität und ein konsistentes Verhalten gewährleistet wird, wo auch immer das Dispatching ausgelöst wird. Dies ist der Schlüssel zur Implementierung einer sauberen Architektur in Ihrer CQRS-basierten Anwendung.
Zum Beispiel führt jede Stelle, die shipment.Dispatch() aufruft, automatisch alle Validierungen und Zustandsübergänge durch. Dies entspricht dem CQRS-Entwurfsmuster und trägt dazu bei, eine klare Trennung zwischen dem Handler und der Domänenlogik zu gewährleisten.
Der Wert der Zentralisierung von Logik
Derek weist darauf hin, dass es bei dieser Art von Änderung nicht darum geht, unnötige Abstraktionen hinzuzufügen. Stattdessen geht es um die Zentralisierung von Logik, die in verschiedenen Teilen Ihres Anwendungscodes verwendet wird. Wenn mehrere Befehlshandler eine Sendung versenden müssen, sollte diese benutzerdefinierte Logik an einem Ort untergebracht werden - innerhalb des Domänenmodells.
Dies macht Ihr Datenmodell robuster und Ihre CQRS-Handler C#-Implementierungen einfacher und wartungsfreundlicher.
Einführung in das Pipeline-Muster
Um den Command Handler weiter zu bereinigen, führt Derek ein Pipeline-Muster ein. Diese Struktur verarbeitet einen Befehl als eine Abfolge von kleinen Einzweckschritten, die jeweils ein Kontextobjekt aufnehmen und den nächsten Schritt aufrufen.
Das Konzept ähnelt dem von ASP.NET Core Middleware, und jeder Schritt konzentriert sich auf einen bestimmten Teil des Ablaufs:
-
Abrufen der Sendung (d. h. Lesen von Daten)
-
Dispatching it (Ausführen von Schreiboperationen)
-
Ereignisse veröffentlichen
- Speichern im Datenspeicher
Diese Schritte verwenden ein gemeinsames Befehlsobjekt, das die Pipeline durchläuft. So entsteht eine saubere und modulare Implementierung der Trennung von Befehls- und Abfrageverantwortung.
Beispiel für die Implementierung der Pipeline
In seiner Beispielimplementierung strukturiert Derek die Pipeline mit Schritten wie:
-
Laden der Sendung - Abrufen der Daten aus der Datenzugriffsschicht unter Verwendung eines Repositorys.
-
Versenden der Sendung - Aufruf der Dispatch()-Methode zur Anwendung der Domänenlogik.
-
Hinzufügen eines Domänenereignisses - Anhängen eines Ereignisses "ShipmentDispatched" an den Kontext.
-
Veröffentlichung von Ereignissen - Versenden von Ereignissen zur Benachrichtigung externer Systeme.
- Änderungen speichern - Aktualisierungen im Datenspeicher beibehalten.
Jeder Schritt stellt einen eigenen Teil der Befehlslogik dar, wodurch die Datenvalidierung verbessert und die Verantwortlichkeiten getrennt werden.
Derek weist außerdem darauf hin, dass E-Mail-Benachrichtigungen jetzt separat behandelt werden, indem auf das Domain-Ereignis reagiert wird. Dies steht im Einklang mit den Grundsätzen des Event Sourcing und fördert die letztendliche Konsistenz.
Vorteile beim Testen und der Wartbarkeit
Einer der größten Vorteile dieses Musters ist die Testbarkeit. Bei einem großen Befehlshandler können mehrere Abhängigkeiten bestehen (z. B. Repositories, Mail-Dienste, Logger). Wenn Sie den Handler in Pipelineschritte aufteilen, benötigt jeder Schritt nur ein paar Abhängigkeiten.
Dieser modulare Ansatz ermöglicht es Ihnen, einzelne Schritte einfach mit Dependency Injection zu testen und bei Bedarf Fakes oder Mocks zu verwenden. Wenn Sie zum Beispiel einen Schritt testen, der Dispatch() aufruft, müssen Sie keinen E-Mail-Dienst oder Event-Publisher nachbilden.
Diese Trennung der Belange folgt dem CQRS-Muster zur Trennung der Verantwortlichkeiten, wodurch Ihre Lese- und Schreibmodelle sauberer und zielgerichteter werden.
Kompatibilität und Wiederverwendbarkeit
Ein weiterer Vorteil des Pipeline-Ansatzes besteht darin, dass er komponierbar ist. Wenn Sie etwas wie das Outbox-Pattern verwenden, können Sie sicherstellen, dass Ereignisse erst veröffentlicht werden, nachdem die Schreibmodelle persistiert wurden. Dieses Maß an Kontrolle ist bei CQRS-Implementierungen, bei denen Konsistenz und Liefergarantien wichtig sind, unerlässlich.
Sie können auch Schritte für verschiedene CQRS-Handler freigeben, z. B. einen generischen Schritt "SaveChanges" oder einen Schritt "ValidateRequest".
Mit Tools wie der MediatR-Bibliothek, die Befehls- und Abfrageverarbeitung unterstützt, können Sie diese Schritte sogar durch Dependency Injection in Ihrer .NET Core-Anwendung unter Verwendung von IServiceCollection-Diensten registrieren.
Um dieses System einzurichten, können Sie Install-Package MediatR über die Paketmanager-Konsole von Visual Studio ausführen - ein üblicher Schritt bei der Implementierung von CQRS in C#.
Die Gegenleistungen
Derek schreckt nicht vor der erhöhten Komplexität zurück, die mit diesem Ansatz einhergeht. Pipelines führen Indirektionen ein, und wenn man sich einen Call Stack ansieht, kann es einem vorkommen, als würde man sich in einem Labyrinth bewegen.
Bei komplexer Geschäftslogik lohnt sich dieser Kompromiss jedoch oft. Wenn ein Handler mehr als 10 Abhängigkeiten und Hunderte von Logikzeilen hat, ermöglicht CQRS den Entwicklern eine bessere Strukturierung und Pflege dieser Abläufe.
Abschließende Überlegungen zur Frage, wann ein Refactor erforderlich ist
Abschließend erinnert Derek die Betrachter daran, sorgfältig zu prüfen, ob ihre CQRS-Handler-Implementierung in C# wirklich aufgebläht ist. Nicht jedes Szenario erfordert eine Pipeline. Sein Ziel ist es, Möglichkeiten aufzuzeigen, und es liegt an den Entwicklern, ihre eigene CQRS-Implementierung zu bewerten und festzustellen, ob solche Muster hilfreich sind.
Er ermutigt Entwickler, nach Bereichen in ihrem Code zu suchen, in denen die Trennung von Belangen dazu beitragen würde, die Konsistenz zu wahren, den Code modularer zu gestalten und Lese- und Schreibvorgänge besser zu verwalten - insbesondere in CQRS-Webanwendungen.
Abschluss
Das Video von Derek Comartin bietet eine praktische Anleitung zur Bereinigung von CQRS-Handlern mithilfe von Domain-Logik-Kapselung und Pipelines. Dieser Ansatz hilft, Probleme mit aufgeblähtem Code zu lösen, fördert die Datenintegrität und verbessert die Wartbarkeit, indem der Anwendungscode in verschiedene Modelle aufgeteilt wird.
Ganz gleich, ob Sie Mitarbeiterdaten, Produktdetails oder einen neuen Benutzerbefehl verarbeiten, die Anwendung des CQRS-Musters mit Pipelines und domänenorientiertem Design wird Ihre Codebasis skalierbarer, testbarer und robuster machen.
Durch die Verwendung von Datenübertragungsobjekten, separaten Modellen und einer klaren Trennung zwischen Lese- und Schreiblogik wird Ihre .NET-Anwendung besser strukturiert und kann im Laufe der Zeit leichter weiterentwickelt werden.


