Definition von Anwendungsfällen
In diesem Abschnitt werden wir untersuchen, wie man die Anwendungsszenarien Ihrer Anwendung mit dem Lino CLI definiert. Anwendungsszenarien stellen spezifische Interaktionen zwischen Benutzern oder externen Systemen und der Anwendung dar, kapseln Geschäftsregeln ein und fördern eine domänenorientierte Architektur. Wir behandeln die Trennung von Lese- und Schreiboperationen durch das CQRS-Muster sowie die Standardisierung der Rückgaben mit dem Result Pattern.
Das CQRS-Muster (Command Query Responsibility Segregation) schlägt vor, Operationen, die den Zustand der Anwendung ändern (Commands), von denen zu trennen, die nur Daten abfragen (Queries). Dieser Ansatz ermöglicht es, jeden Operationstyp unabhängig zu optimieren, was die Skalierbarkeit, Leistung und Wartbarkeit des Systems verbessert.
Das Result Pattern wird verwendet, um die RĂĽckgabe von Operationen zu standardisieren, indem Informationen ĂĽber Erfolg oder Misserfolg, Fehlermeldungen und Ergebnisdaten gekapselt werden. Dies erleichtert die konsistente Behandlung von Antworten in verschiedenen Schichten der Anwendung.
Wir werden uns ansehen, wie die Dateien für Commands und Queries strukturiert sind, wie die Datenvalidierung erfolgt, wie die Geschäftslogik in Handlers angewendet wird und wie Ergebnisse standardisiert zurückgegeben werden. All dies geschieht durch die automatische Erstellung dieser Komponenten mittels des Lino CLI.
Hinweis: Obwohl es nicht zwingend erforderlich ist, bietet Lino derzeit zwei Optionen zur Anwendung des CQRS-Musters auf Anwendungsebene an: Die Verwendung des Mediator-Musters ĂĽber die Bibliothek MediatR
(von Jimmy Bogard) oder die Bibliothek Mediator
(von Martin Othamar).
Überblick über Anwendungsfälle
Ein Anwendungsfall stellt eine vollständige Interaktion zwischen Benutzern oder externen Systemen und der Anwendung dar und beschreibt spezifische Geschäftsszenarien. In Lino wird jeder Anwendungsfall unterteilt in:
- Command: stellt die Absicht dar, den Systemzustand zu ändern (erstellen, aktualisieren, löschen usw.).
- Query: stellt die Absicht dar, Daten abzufragen, ohne den Domänenzustand zu ändern.
Diese Trennung fördert die Klarheit des Codes, erleichtert Tests, ermöglicht unabhängige Skalierbarkeit für Lese- und Schreibvorgänge und entspricht den Prinzipien der Clean Architecture sowie bewährten Praktiken des Domain-Driven Design (DDD).
Commands
Ein Command ist eine unveränderliche Nachricht, die nur die Daten enthält, die erforderlich sind, um eine Aktion auszuführen, die den Systemzustand ändert (z. B. CreateInvoice
, DeactivateUser
).
Es sollte nur die Eigenschaften enthalten, die die wesentlichen Informationen zur DurchfĂĽhrung der Operation darstellen.
Merkmale eines Commands
- Unveränderlichkeit: Implementiert als
record
oder Klasse nur mitget
, ohne öffentliche Setter. - Imperativer Name: Reflektiert die auszuführende Aktion, z. B.
CreateOrder
,UpdateCustomerAddress
. - Minimale Daten: Enthält nur die Felder, die für die Ausführung der Operation notwendig sind, ohne große Datenmengen zurückzugeben.
- Isolierte Validierung: Jeder Command hat eigene Validierungsregeln, die sicherstellen, dass er konsistent ist, bevor er den Handler erreicht.
Command Validators
Die Command Validators stellen sicher, dass der Command wohlgeformt ist und die Geschäftsanforderungen erfüllt, bevor er an den Handler gesendet wird. Im Lino verwenden wir die Bibliothek FluentValidation zur Implementierung dieser Validierungen, da sie in .NET-Projekten weit verbreitet ist und eine Fluent API zur Regeldefinition bietet.
Gängige Validierungsregeln
NotEmpty
,NotNull
fĂĽr Pflichtfelder.InclusiveBetween
für Zahlenbereiche (z. B. Geldwerte, Mindest-/Höchstmenge).MaximumLength
undMinimumLength
für String-Längen.RuleForEach
zur Validierung von Elementen in Sammlungen (List<T>
).Must
fĂĽr benutzerdefinierte Regeln (z. B. Dokumentenformate, Datumsvalidierung).
Command Handlers
Der Command Handler ist verantwortlich für die Ausführung der domänenspezifischen Logik, die mit diesem Command verbunden ist. Er orchestriert Repositories, Unit of Work (IUnitOfWork
), Hilfsdienste und löst bei Bedarf Domain Events aus.
Implementierungsmuster eines Handlers
- Empfang von Abhängigkeiten (Repositories, externe Dienste, UnitOfWork) über Dependency Injection.
- Mapping und Instanziierung von Domänenentitäten zur Gewährleistung der Konsistenz.
- Anwendung von Geschäftsregeln (zusätzliche Validierungen, Wertberechnungen, Auslösen von Domain Events).
- Persistieren der Änderungen über
IUnitOfWork
oder direkte Repositories. - RĂĽckgabe eines Command Result, das Erfolg oder Fehler anzeigt (
Result<T>
).
Command Results und Result Pattern
Das Command Result sollte ein einfaches DTO sein, das nur die minimal notwendigen Daten enthält, damit der Aufrufer (z. B. eine API oder ein Frontend) weiterarbeiten kann. Es enthält üblicherweise die ID der erstellten oder aktualisierten Entität (Id
) und im Fehlerfall standardisierte Fehlerinformationen.
Für Szenarien, in denen der Command fehlschlagen kann, verwenden wir das Result Pattern: eine Abstraktion, die das Ergebnis einer Operation kapselt und Erfolg oder Fehler darstellen kann. In .NET werden häufig Typen wie Result<T>
(oder ähnliche Bibliotheken) verwendet, die:
- Ermöglichen, im Erfolgsfall einen
Value
zu definieren (z. B. ein einfaches DTO). - Im Fehlerfall standardisierte Codes oder Nachrichten speichern (
Error
), eventuell mit zusätzlichen Metadaten (HTTP-Codes, Validierungsdetails, etc.). - Die Vereinheitlichung des Fehlerhandlings über die gesamte API erleichtern und Konsistenz zwischen Schichten sicherstellen.
Erstellen eines Commands mit dem CLI
Lino vereinfacht die Generierung aller notwendigen Artefakte fĂĽr einen neuen Command durch den Befehl:
lino command new
Beim AusfĂĽhren dieses Befehls fragt der interaktive Assistent:
- Service — Name des Dienstes, in dem der Command erstellt wird.
- Modul — In modularisierten Diensten das Zielmodul.
- Entität — Die Domänenentität, die mit dem Command verbunden ist (optional, aber empfohlen).
- Name des Commands — Der endgültige Name des Commands (z. B.
CreateOrder
).
Nach Bestätigung erstellt Lino automatisch folgende Dateien:
CreateOrderCommand.cs
CreateOrderCommandValidator.cs
CreateOrderCommandHandler.cs
CreateOrderCommandResult.cs
Beispielhafte generierte Struktur
Betrachten Sie den Command CreatePerson
. Die generierte Struktur sieht etwa so aus:
MyApp/ └── src/ └── Services/ └── MyService/ └── Application/ ├── MyApp.MyService.Application.csproj └── UseCases/ └── People/ ├── Commands/ │ └── CreatePerson/ │ ├── CreatePersonCommand.cs │ ├── CreatePersonCommandValidator.cs │ ├── CreatePersonCommandHandler.cs │ └── CreatePersonCommandResult.cs └── Queries/ └── ...
Abfragen
Eine Abfrage stellt die Absicht dar, Daten zu erhalten, ohne den Zustand der Domäne zu verändern. Abfragen sind für eine effiziente Lesbarkeit konzipiert und geben nur die erforderlichen Felder zurück, ohne ganze Entitäten zu laden, wenn dies nicht erforderlich ist.
Eigenschaften einer Abfrage
- Unveränderlich — wie Befehle (Commands) sollte eine Abfrage nach ihrer Erstellung nicht mehr verändert werden.
- Beschreibender Name — spiegelt die gesuchten Informationen wider, z. B.
GetCustomerById
,ListOrdersByDateRange
. - Filter und Paginierung — kann Parameter für spezifische Suchen enthalten (Daten, Status, Seite, Sortierung).
- Projektion — sollte ein DTO oder View Model mit nur den benötigten Feldern zurückgeben, um das Laden kompletter Domänenobjekte zu vermeiden.
Abfrage-Validatoren
Abfrage-Validatoren sind dafĂĽr verantwortlich, Eingabeparameter wie Filter, Paginierungswerte (page, pageSize) und Sicherheitsregeln (Benutzerberechtigungen, Datenzugriffsrechte) zu validieren. Sie werden ebenfalls mit FluentValidation implementiert.
Gängige Validierungsregeln für Abfragen
GreaterThanOrEqualTo
oderLessThanOrEqualTo
für Bereichsfilter (z. B. Daten).Length
für Textfilter (z. B. Suche nach Name, E-Mail).InclusiveBetween
für Paginierungsparameter (z. B. minimale/maximale Anzahl von Elementen pro Seite).
Abfrage-Handler
Der Abfrage-Handler ist dafür zuständig, Repositories oder den Datenbankkontext abzufragen und optimierte Projektionen zurückzugeben, ohne vollständige Domänenentitäten zu laden.
In Lino wird empfohlen, Projektionen mit Methoden wie .Select()
zu verwenden, um Daten direkt im erwarteten DTO-Format abzubilden, was Effizienz und Leistung verbessert.
Abfrageergebnisse
In Lino werden Abfrageergebnisse stets durch Records mit dem Suffix QueryResult
dargestellt.
Diese Konvention gilt unabhängig vom Antworttyp – sei es eine Liste von Elementen (mit oder ohne Paginierung) oder ein einzelnes detailliertes Element.
Erstellen einer Abfrage mit dem CLI
Ähnlich wie bei Commands stellt Lino folgenden Befehl zur Verfügung:
lino query new
Der Assistent fragt nach:
- Service — Der Dienst, in dem die Abfrage erstellt wird.
- Modul — Falls zutreffend, das zugehörige Domänenmodul.
- Entität — Die Entität oder das Aggregat, dem die Abfrage zugeordnet ist (optional).
- Abfragename — Zum Beispiel
GetOrderById
oderListProductsByCategory
.
Lino generiert automatisch:
GetOrderByIdQuery.cs
GetOrderByIdQueryValidator.cs
GetOrderByIdQueryHandler.cs
GetOrderByIdQueryResult.cs
Beispiel fĂĽr eine generierte Struktur fĂĽr Abfragen
MyApp/ └── src/ └── Services/ └── MyService/ └── Application/ ├── MyApp.MyService.Application.csproj └── UseCases/ └── Orders/ ├── Commands/ | └── ... └── Queries/ └── GetOrderById/ ├── GetOrderByIdQuery.cs ├── GetOrderByIdQueryValidator.cs ├── GetOrderByIdQueryHandler.cs └── GetOrderByIdQueryResult.cs
Fazit
Nachdem Sie verstanden haben, wie man Use Cases (Commands und Queries) in Lino definiert und strukturiert, sind Sie bereit, robuste und skalierbare Geschäftsszenarien zu erstellen, indem Sie die Schreib- und Leselogik klar trennen. Nutzen Sie das CLI, um die Entwicklung zu beschleunigen, überprüfen und passen Sie jedoch die Implementierungen stets an die spezifischen Anforderungen Ihrer Domäne an.
Im nächsten Schritt erkunden Sie, wie Sie Persistenz, Domänenereignisse und Infrastrukturdienste handhaben, um die gesamte Architektur Ihrer Anwendung zu gestalten.