Arbeiten mit Events
In modernen Systemen, die sich an Architektur-Best-Practices wie DDD (Domain-Driven Design) und Clean Architecture orientieren, sind Events grundlegende Mechanismen, um ZustandsĂ€nderungen zu modellieren und asynchrone Kommunikation zwischen Komponenten oder Systemen zu ermöglichen. Ein Event reprĂ€sentiert etwas, das bereits im AnwendungsdomĂ€nenbereich passiert ist und fĂŒr andere Teile des Systems oder externe Dienste von Interesse sein kann. Ereignisse in Lino umfassen auĂerdem shadow entities und den Transactional Outbox Flow mit DDD, MediatR, MassTransit, RabbitMQ, Hangfire und Unit of Work fĂŒr zuverlĂ€ssige Nachrichten.
DomÀnenereignisse
DomĂ€nenereignisse stellen wichtige Fakten dar, die im Kontext der Anwendung aufgetreten sind. Sie werden intern im System erzeugt und konsumiert, wodurch ZustandsĂ€nderungen entkoppelt propagiert werden können, das heiĂt, die Objekte mĂŒssen sich nicht direkt kennen.
Wann sollte ein DomÀnenereignis verwendet werden?
- Immer dann, wenn eine Operation eine signifikante Ănderung im DomĂ€nenmodell erzeugt und es notwendig ist, andere Teile des Systems zu benachrichtigen oder auszulösen.
- Um das DomÀnenmodell kohÀrent zu halten und es zu ermöglichen, dass verschiedene Prozesse ausgelöst werden, ohne direkte AbhÀngigkeiten zwischen Modulen oder Services zu erzeugen.
In Lino ist das Erstellen eines neuen DomĂ€nenereignisses einfach. Sie können ausfĂŒhren:
lino event new lino event new --name <EventName> --service <ServiceName> --module <ModuleName> --entity <EntityName> lino event edit --service <ServiceName> --module <ModuleName> --entity <EntityName> lino event list --service <ServiceName> --module <ModuleName> --entity <EntityName>
Der CLI-Assistent wird Folgendes abfragen:
- Service â Der Service, in dem das Ereignis erstellt wird.
- Modul â Das Modul, in dem das Ereignis erstellt wird (nur in modularen Services).
- EntitĂ€t â Die EntitĂ€t, in der das Ereignis erstellt/assoziiert wird.
- Ereignistyp â DomĂ€nenereignis oder Integrationsereignis.
- Ereignisname â Im DomĂ€nenkontext verwendeter Name, der mit der EntitĂ€t verknĂŒpft wird.
Beispiel
Beim Erstellen des DomĂ€nenereignisses UserCreated, das mit der EntitĂ€t User verknĂŒpft ist,
erstellt das System automatisch ein Ereignis mit dem Namen UserCreatedDomainEvent.
Dieser Name macht fĂŒr jeden Teil des Systems, der das Ereignis konsumiert, deutlich, dass die Benutzererstellung bereits abgeschlossen ist.
Von Lino generierte Struktur:
<ProjectName>/
âââ src/
âââ Services/
âââ <ServiceName>/
âââ Domain/
âââ <ProjectName>.<ServiceName>.Domain.csproj
âââ Aggregates/
âââ Users/
âââ User.cs
âââ Errors/
âââ Events/
â âââ UserCreatedDomainEvent.cs
âââ Repositories/
âââ Resources/
Technischer Ablauf in Lino
- Das aggregate registriert ein
IDomainEventĂŒberRegisterDomainEvent. - Der command handler speichert ĂŒber
IUnitOfWork, bevorzugt mitSaveChangesInTransactionAsyncwenn Events beteiligt sind. - Die Unit of Work persistiert Ănderungen und veröffentlicht Events ĂŒber MediatR.
- Ein domain handler kann ein integration event in
IOutboxregistrieren.
Events sind keine Commands; verwenden Sie Vergangenheitsnamen wie UserCreatedDomainEvent.
DomÀnen-Ereignis-Handler
Ein Domain Event Handler ist eine Klasse, die dafĂŒr verantwortlich ist, auf ein DomĂ€nen-Ereignis zu reagieren und Aktionen im Zusammenhang mit dem internen Zustand der Anwendung auszufĂŒhren, immer innerhalb desselben Transaktionskontexts.
Der Hauptzweck dieser Handler besteht darin, das System kohĂ€rent und entkoppelt zu halten, sodass zusĂ€tzliche Regeln angewendet werden können, ohne die zentrale Logik der EntitĂ€t oder des Aggregats zu ĂŒberlasten.
Zum Beispiel kann es nach der Erstellung eines User erforderlich sein, Statistiken zu aktualisieren, ein internes Protokoll zu erstellen oder ein anderes Aggregat zu benachrichtigen.
Diese Aktionen machen im Kontext der DomÀne Sinn und können synchron erfolgen, wodurch sofortige Konsistenz gewÀhrleistet wird.
Operationen, die jedoch von externen Ressourcen abhĂ€ngen â wie das Versenden von E-Mails oder das Aufrufen von Drittanbieter-APIs â sollten nicht direkt aus DomĂ€nenereignissen ausgefĂŒhrt werden, da dies die Transaktion an langsame oder instabile Aufgaben binden wĂŒrde. In solchen FĂ€llen kann die DomĂ€ne ein Integrationsereignis erzeugen (im Outbox-Mechanismus protokolliert), das spĂ€ter asynchron und resilient verarbeitet wird.
Hauptmerkmale:
- Reagiert auf ein Ereignis, Àndert es jedoch niemals.
- FĂŒhrt nur In-Process-Operationen aus, die mit der Konsistenz der DomĂ€ne zusammenhĂ€ngen.
- Stellt sicher, dass alles innerhalb derselben Transaktion ausgefĂŒhrt wird.
Um einen neuen DomĂ€nen-Ereignis-Handler zu erstellen, fĂŒhren Sie einfach den Befehl aus:
lino event-handler new
Der CLI-Assistent wird Folgendes abfragen:
- Service â Der Service, in dem der Ereignis-Handler erstellt wird.
- Modul â Das Modul, in dem der Ereignis-Handler erstellt wird (nur in modularen Services).
- EntitĂ€t â Die EntitĂ€t, in der der Ereignis-Handler erstellt wird.
- Ereignistyp â DomĂ€nenereignis oder Integrationsereignis.
- Ereignis â Das zu konsumierende Ereignis.
- Name des Ereignis-Handlers â Der Name, der mit der EntitĂ€t und dem DomĂ€nenereignis verknĂŒpft wird.
Beispiel
Beim Erstellen des DomĂ€nen-Ereignis-Handlers UserCreated, der mit der EntitĂ€t User und dem Ereignis UserCreatedDomainEvent verknĂŒpft ist, erstellt das System automatisch einen Ereignis-Handler mit dem Namen UserCreatedDomainEventHandler.
Von Lino generierte Struktur:
<ProjectName>/
âââ src/
âââ Services/
âââ <ServiceName>/
âââ Application/
âââ <ProjectName>.<ServiceName>.Application.csproj
âââ UseCases/
âââ Users/
âââ Commands/
âââ EventHandlers/
â âââ Domain/
â âââ UserCreatedDomainEventHandler.cs
âââ Logging/
âââ Queries/
âââ Resources/
Technische Verantwortlichkeiten
- Implementiert
IDomainEventHandlerund reagiert innerhalb der Anwendung. - FĂŒhrt interne Konsistenz, logging, tracing oder Integrationsregistrierung aus.
- Verwendet
IOutbox.RegisterIntegrationEventfĂŒr zuverlĂ€ssige Nachrichten. - HĂ€lt Business-Ănderung und
OutboxMessagein derselben Transaktion.
Integrationsereignisse
Integrationsereignisse sind Nachrichten, die darauf hinweisen, dass etwas Wichtiges passiert ist, und die mit externen Systemen oder anderen Microservices geteilt werden mĂŒssen.
Im Gegensatz zu DomÀnenereignissen besteht hier das Ziel in der Kommunikation zwischen Systemen und der Synchronisation von ZustÀnden.
Wann ein Integrationsereignis erstellen:
- Wenn eine Ănderung in Ihrem System in einem anderen System widergespiegelt werden muss.
- Wenn Ihr Microservice Ănderungen veröffentlichen muss, damit andere Microservices darauf reagieren können.
Hauptunterschiede zwischen Domain Events und Integration Events:
| Aspekt | DomÀnenereignis | Integrationsereignis |
|---|---|---|
| Zielgruppe | Intern | Extern |
| Kopplung | Niedrig (intern) | Notwendig (zwischen Systemen) |
| Verarbeitungszeit | Sofort | Kann asynchron sein, mit Liefergarantie |
| Erforderliche Persistenz | Nicht erforderlich | Ja (fĂŒr ZuverlĂ€ssigkeit und Resilienz) |
In Lino ist das Erstellen eines neuen Integrationsereignisses einfach. Sie können ausfĂŒhren:
lino event new lino event new --name <EventName> --service <ServiceName> --module <ModuleName> --entity <EntityName> lino event list --service <ServiceName> --module <ModuleName> --entity <EntityName>
Der CLI-Assistent fragt nach:
- Service â Der Service, in dem das Ereignis erstellt wird.
- Modul â Das Modul, in dem das Ereignis erstellt wird (nur fĂŒr modulare Services).
- EntitĂ€t â Die EntitĂ€t, in der das Ereignis erstellt / zugeordnet wird.
- Ereignistyp â DomĂ€nenereignis oder Integrationsereignis.
- Ereignisname â Name, der fĂŒr das Integrationsereignis verwendet wird und mit der EntitĂ€t verknĂŒpft ist.
Beispiel
Wenn das Integrationsereignis UserCreated mit der EntitÀt User erstellt wird, erstellt das System automatisch ein Ereignis mit dem Namen UserCreatedIntegrationEvent.
Dieser Name macht fĂŒr jeden Teil des Systems, der das Ereignis konsumiert, deutlich, dass die Benutzererstellung bereits abgeschlossen ist.
Von Lino generierte Struktur:
<ProjectName>/
âââ src/
âââ Services/
âââ <ServiceName>/
âââ Integration.Events/
âââ <ProjectName>.<ServiceName>.Integration.Events.csproj
âââ Users/
âââ UserCreatedIntegrationEvent.cs
ZuverlÀssige Veröffentlichung
Ein integration event implementiert IIntegrationEvent und ĂŒberschreitet Modul-, Service- oder Systemgrenzen. Die Anwendung registriert das Event in IOutbox, die Unit of Work speichert ein OutboxMessage, und ein Hangfire worker veröffentlicht spĂ€ter ĂŒber MassTransit/RabbitMQ.
Integrations-Event-Handler
Ein Integration Event Handler ist eine Klasse, die dafĂŒr verantwortlich ist, ein Integrationsereignis zu konsumieren, das normalerweise von einem anderen Service oder Kontext veröffentlicht wird, und anschlieĂend spezifische Aktionen in der eigenen DomĂ€ne auszufĂŒhren.
Diese Handler empfangen Ereignisse ĂŒber Messaging-Systeme (wie RabbitMQ, Kafka, Azure Service Bus usw.), meist in Kombination mit dem Outbox-Pattern, das eine zuverlĂ€ssige Zustellung und asynchrone Verarbeitung sicherstellt.
Zum Beispiel kann, wenn ein UserCreated-Ereignis von einem Identity-Service veröffentlicht wird, der entsprechende Handler in einem anderen Kontext reagieren, indem er eine Willkommens-E-Mail sendet oder externe APIs aufruft.
Diese VorgĂ€nge können langsamer oder fehleranfĂ€llig sein, beeintrĂ€chtigen jedoch die interne Konsistenz der Anwendung nicht, da sie auĂerhalb der Haupttransaktion behandelt werden.
Hauptmerkmale:
- Reagiert auf Ereignisse, die geschĂ€ftlich relevante Fakten fĂŒr andere Kontexte darstellen.
- FĂŒhrt VorgĂ€nge aus, die langsam oder extern sein können (z.âŻB. E-Mail-Versand, API-Aufrufe).
- Wird asynchron und resilient verarbeitet, oft mit Wiederholungen und Monitoring.
- Stellt sicher, dass externe Fehler die ursprĂŒngliche DomĂ€nentransaktion nicht beeinflussen.
- Erleichtert die Integration zwischen Bounded Contexts und verteilten Systemen.
Um einen neuen Integrations-Event-Handler zu erstellen, fĂŒhren Sie einfach folgenden Befehl aus:
lino event-handler new lino event-handler new --name <EventHandlerName> --service <ServiceName> --module <ModuleName> --entity <EntityName> lino event-handler list --service <ServiceName> --module <ModuleName> --entity <EntityName>
Der CLI-Assistent fragt Folgendes ab:
- Service â Der Service, in dem der Event-Handler erstellt wird.
- Modul â Das Modul, in dem der Event-Handler erstellt wird (nur in modularen Services).
- EntitĂ€t â Die EntitĂ€t, in der der Event-Handler erstellt wird.
- Ereignistyp â DomĂ€nenereignis oder Integrationsereignis.
- Service â Der Service, in dem das zu konsumierende Ereignis existiert.
- Modul â Das Modul, in dem das zu konsumierende Ereignis existiert (nur in modularen Services).
- EntitĂ€t â Die EntitĂ€t, in der das zu konsumierende Ereignis existiert.
- Ereignis â Das zu konsumierende Ereignis.
- Name des Event-Handlers â Name, der mit der EntitĂ€t und dem Integrationsereignis verknĂŒpft wird.
Beispiel
Beim Erstellen des Integrations-Event-Handlers SendEmailOnUserCreated, der mit der EntitĂ€t User und dem Ereignis UserCreatedIntegrationEvent verknĂŒpft ist,
erstellt das System automatisch einen Event-Handler mit dem Namen SendEmailOnUserCreatedIntegrationEventHandler.
Von Lino generierte Struktur:
<ProjectName>/
âââ src/
âââ Services/
âââ <ServiceName>/
âââ Application/
âââ <ProjectName>.<ServiceName>.Application.csproj
âââ UseCases/
âââ Users/
âââ Commands/
âââ EventHandlers/
â âââ Integration/
â âââ SendEmailOnUserCreatedIntegrationEventHandler.cs
âââ Logging/
âââ Queries/
âââ Resources/
Operative Hinweise
- Implementiert
IIntegrationEventHandlerund konsumiert MassTransit Nachrichten. - Liest nicht direkt die Datenbank des Producers.
- Aktualisiert lokalen Zustand, Projektionen oder shadow entities.
- Sollte idempotent sein, weil retries und doppelte Nachrichten möglich sind.
Shadow Entities
Eine shadow entity ist eine lokale, minimale und kontrollierte Kopie von Daten, deren Owner in einem anderen Modul oder Service liegt. Sie reduziert Kopplung, wenn der Consumer Referenzdaten abfragen, validieren oder anzeigen muss, ohne direkt auf die Producer-Datenbank zuzugreifen.
Shadow Entities sind nĂŒtzlich, wenn eventual consistency akzeptabel ist. Der Producer veröffentlicht ein Integration Event, die Nachricht wird in der Outbox persistiert, der worker veröffentlicht sie auf dem Bus, und der Consumer aktualisiert seine lokale Kopie ĂŒber einen integration event handler.
lino shadow-entity new lino event new lino event-handler new
Der Befehl ist auch ĂŒber den Alias lino shadow new verfĂŒgbar. Im interaktiven Ablauf fragt Lino nach Ziel-Service oder Ziel-Modul, Source Entity und den zu kopierenden Properties. Dadurch mĂŒssen Identifier-Typ, Pflichtfeldstatus, string-LĂ€nge und grundlegende Referenzen nicht manuell neu erstellt werden, was Inkonsistenzen zwischen Source und Consumer reduziert.
Die shadow entity ist keine vollstĂ€ndige Replik. Im Beispiel eines Catalog-Moduls, das Daten aus Tenancy und Security konsumiert, kann der Catalog nur Tenant.Id, Tenant.Slug, User.Id, User.Email und User.TenantId speichern. Die ursprĂŒngliche User-Entity enthĂ€lt weiterhin Hash, BestĂ€tigung, Tokens, Datumswerte und andere interne Felder, die nicht zum Catalog-Kontext gehören.
Wann verwenden
- Wenn der Consumer wenige Felder einer Entity benötigt, die zu einem anderen Kontext gehört.
- Wenn die Regel eine kurze Verzögerung zwischen Producer-Ănderung und Consumer-Aktualisierung akzeptiert.
- Wenn direktes Lesen aus der Producer-Datenbank eine unzulĂ€ssige Kopplung zwischen Modulen oder Services erzeugen wĂŒrde.
- Wenn grids, Filter, lokale Validierungen oder Projektionen externe Referenzdaten benötigen.
Wann vermeiden
- Wenn die Regel sofort aktuelle Daten verlangt und keine eventual consistency akzeptiert.
- Wenn die lokale Kopie zu einer vollstĂ€ndigen Replik des ursprĂŒnglichen aggregate wĂŒrde.
- Wenn der Consumer beginnt, von Invarianten abzuhÀngen, die dem Producer gehören.
Empfohlener Ablauf
- Modelliere die shadow entity nur mit den Feldern, die der Consumer benötigt.
- Erstelle oder wÀhle das vom Producer veröffentlichte integration event aus.
- Erstelle einen integration event handler im Consumer.
- Implementiere einen idempotenten upsert mit dem Producer-SchlĂŒssel oder einem natĂŒrlichen Identifier.
- Protokolliere genug Informationen, um retries, doppelte Nachrichten und permanente Fehler zu diagnostizieren.
Event-driven Synchronisierung
Der vollstÀndige Ablauf kombiniert Domain, Application, Unit of Work, Outbox, worker, bus und Consumer. Ziel ist es, fragile direkte Aufrufe zwischen Modulen oder Services zu vermeiden und retry, Auditing und Reprocessing zu ermöglichen, wenn Veröffentlichung oder Verarbeitung fehlschlÀgt.
- Ein aggregate Àndert sich und registriert ein Domain Event.
- Die Unit of Work speichert die Ănderung innerhalb einer Transaktion.
- Ein domain handler reagiert auf den Fakt und registriert ein Integration Event in
IOutbox. - Die Unit of Work persistiert Business-Daten und
OutboxMessagein derselben Transaktion. - Ein Hangfire worker liest die Outbox und veröffentlicht ĂŒber MassTransit/RabbitMQ auf dem bus.
- Der Consumer empfĂ€ngt die Nachricht ĂŒber einen
IIntegrationEventHandler. - Der Consumer aktualisiert sein lokales Modell, shadow entity, Projektion, E-Mail, externe Integration oder einen anderen asynchronen Ablauf.
Domain change -> Domain Event -> Domain Handler -> Integration Event -> Outbox -> Worker -> Event Bus -> Consumer -> Local model
Designregeln
- Lege Invarianten in die Domain; verwende Events fĂŒr Reaktionen, nicht um verpflichtende Regeln zu verstecken.
- Registriere integration events in derselben Transaktion wie die Business-Ănderung.
- Halte IntegrationsvertrÀge klein, versionierbar und stabil.
- Implementiere idempotente consumers, weil retries und doppelte Nachrichten auftreten können.
- Ăberwache worker-Fehler und Nachrichten, die in der Outbox hĂ€ngen bleiben.
Praktisches Ergebnis: Der Producer muss die Consumer nicht kennen, Consumer greifen nicht auf die Producer-Datenbank zu, und die Kommunikation zwischen Kontexten bleibt resilient, selbst wenn broker oder Consumer vorĂŒbergehend nicht verfĂŒgbar ist.
