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

  1. Das aggregate registriert ein IDomainEvent ĂŒber RegisterDomainEvent.
  2. Der command handler speichert ĂŒber IUnitOfWork, bevorzugt mit SaveChangesInTransactionAsync wenn Events beteiligt sind.
  3. Die Unit of Work persistiert Änderungen und veröffentlicht Events ĂŒber MediatR.
  4. Ein domain handler kann ein integration event in IOutbox registrieren.

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 IDomainEventHandler und reagiert innerhalb der Anwendung.
  • FĂŒhrt interne Konsistenz, logging, tracing oder Integrationsregistrierung aus.
  • Verwendet IOutbox.RegisterIntegrationEvent fĂŒr zuverlĂ€ssige Nachrichten.
  • HĂ€lt Business-Änderung und OutboxMessage in 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 IIntegrationEventHandler und 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

  1. Modelliere die shadow entity nur mit den Feldern, die der Consumer benötigt.
  2. Erstelle oder wÀhle das vom Producer veröffentlichte integration event aus.
  3. Erstelle einen integration event handler im Consumer.
  4. Implementiere einen idempotenten upsert mit dem Producer-SchlĂŒssel oder einem natĂŒrlichen Identifier.
  5. 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.

  1. Ein aggregate Àndert sich und registriert ein Domain Event.
  2. Die Unit of Work speichert die Änderung innerhalb einer Transaktion.
  3. Ein domain handler reagiert auf den Fakt und registriert ein Integration Event in IOutbox.
  4. Die Unit of Work persistiert Business-Daten und OutboxMessage in derselben Transaktion.
  5. Ein Hangfire worker liest die Outbox und veröffentlicht ĂŒber MassTransit/RabbitMQ auf dem bus.
  6. Der Consumer empfĂ€ngt die Nachricht ĂŒber einen IIntegrationEventHandler.
  7. 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.

Ein unbehandelter Fehler ist aufgetreten. Aktualisieren 🗙