Travailler avec les événements

Dans les systĂšmes modernes, suivant les bonnes pratiques d’architecture telles que DDD (Domain-Driven Design) et Clean Architecture, les Ă©vĂ©nements sont des mĂ©canismes fondamentaux pour modĂ©liser les changements d’état et la communication asynchrone entre composants ou systĂšmes. Un Ă©vĂ©nement reprĂ©sente quelque chose qui s’est dĂ©jĂ  produit dans le domaine de l’application et qui peut intĂ©resser d’autres parties du systĂšme ou des services externes. ComplĂ©ment: This section also covers shadow entities and the Transactional Outbox flow with DDD, MediatR, MassTransit, RabbitMQ, Hangfire, and Unit of Work for reliable messaging.

ÉvĂ©nements de Domaine

Les événements de domaine représentent des faits importants survenus dans le contexte de l'application. Ils sont générés et consommés en interne dans le systÚme, permettant la propagation des changements d'état de maniÚre découplée, c'est-à-dire sans que les objets aient besoin de se connaßtre directement.

Quand utiliser un ÉvĂ©nement de Domaine ?

  • Chaque fois qu'une opĂ©ration gĂ©nĂšre un changement significatif dans le domaine et qu'il est nĂ©cessaire de notifier ou d'activer d'autres parties du systĂšme.
  • Pour maintenir le modĂšle de domaine cohĂ©sif, permettant Ă  diffĂ©rents processus d'ĂȘtre dĂ©clenchĂ©s sans crĂ©er de dĂ©pendances directes entre les modules ou services.

Dans Lino, créer un nouvel événement de domaine est simple. Vous pouvez exécuter :

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>

L'assistant CLI demandera :

  • Service – Le service dans lequel l'Ă©vĂ©nement sera créé.
  • Module – Le module dans lequel l'Ă©vĂ©nement sera créé (uniquement pour les services modulaires).
  • EntitĂ© – L'entitĂ© dans laquelle l'Ă©vĂ©nement sera créé/associĂ©.
  • Type d'Ă©vĂ©nement – ÉvĂ©nement de domaine ou Ă©vĂ©nement d'intĂ©gration.
  • Nom de l'Ă©vĂ©nement – Nom utilisĂ© dans le domaine, associĂ© Ă  l'entitĂ©.

Exemple

En créant l'événement de domaine UserCreated associé à l'entité User, le systÚme crée automatiquement un événement nommé UserCreatedDomainEvent. Ce nom indique clairement à toute partie du systÚme consommant l'événement que l'action de création de l'utilisateur a déjà été effectuée.

Structure générée par Lino :

<ProjectName>/
└── src/
    └── Services/
        └── <ServiceName>/
            └── Domain/
                ├── <ProjectName>.<ServiceName>.Domain.csproj
                └── Aggregates/
                    └── Users/
                        ├── User.cs
                        ├── Errors/
                        ├── Events/
                        │   └── UserCreatedDomainEvent.cs
                        ├── Repositories/
                        └── Resources/

Technical flow in Lino

  1. The aggregate registers an IDomainEvent with RegisterDomainEvent.
  2. The command handler saves through IUnitOfWork, preferably SaveChangesInTransactionAsync when events are involved.
  3. The Unit of Work persists changes and publishes events through MediatR.
  4. A domain handler can register an integration event in IOutbox.

Events are not commands; use past-tense names such as UserCreatedDomainEvent.

Gestionnaires d'ÉvĂ©nements de Domaine

Un Domain Event Handler est une classe responsable de rĂ©agir Ă  un ÉvĂ©nement de Domaine, exĂ©cutant des actions liĂ©es Ă  l'Ă©tat interne de l'application, toujours dans le mĂȘme contexte transactionnel.

L'objectif principal de ces gestionnaires est de maintenir le systÚme cohésif et découplé, permettant l'application de rÚgles supplémentaires sans surcharger la logique centrale de l'entité ou de l'agrégat.

Par exemple, aprĂšs la crĂ©ation d'un User, il peut ĂȘtre nĂ©cessaire de mettre Ă  jour les statistiques, gĂ©nĂ©rer un journal interne ou notifier un autre agrĂ©gat. Ces actions ont du sens dans le domaine et peuvent se produire de maniĂšre synchrone, garantissant une cohĂ©rence immĂ©diate.

Cependant, les opĂ©rations qui dĂ©pendent de ressources externes — telles que l'envoi d'e-mails ou les appels Ă  des API tierces — ne doivent pas ĂȘtre exĂ©cutĂ©es directement Ă  partir des Ă©vĂ©nements de domaine, car cela lierait la transaction Ă  des tĂąches lentes ou instables. Dans ces cas, le domaine peut gĂ©nĂ©rer un Ă©vĂ©nement d'intĂ©gration (enregistrĂ© dans l'Outbox), qui sera traitĂ© ultĂ©rieurement de maniĂšre asynchrone et rĂ©siliente.

Caractéristiques principales :

  • RĂ©agit Ă  un Ă©vĂ©nement mais ne le modifie jamais.
  • ExĂ©cute uniquement des opĂ©rations in-process liĂ©es Ă  la cohĂ©rence du domaine.
  • Garantit que tout s'exĂ©cute dans la mĂȘme transaction.

Pour créer un nouveau gestionnaire d'événements de domaine, il suffit d'exécuter la commande :

lino event-handler new

L'assistant CLI demandera :

  • Service – Service dans lequel le gestionnaire d'Ă©vĂ©nements sera créé.
  • Module – Module dans lequel le gestionnaire d'Ă©vĂ©nements sera créé (uniquement dans les services modulaires).
  • EntitĂ© – EntitĂ© dans laquelle le gestionnaire d'Ă©vĂ©nements sera créé.
  • Type d'Ă©vĂ©nement – ÉvĂ©nement de domaine ou Ă©vĂ©nement d'intĂ©gration.
  • ÉvĂ©nement – ÉvĂ©nement qui sera consommĂ©.
  • Nom du gestionnaire d'Ă©vĂ©nements – Nom utilisĂ© qui sera associĂ© Ă  l'entitĂ© et Ă  l'Ă©vĂ©nement de domaine.

Exemple

En créant le gestionnaire d'événements de domaine UserCreated associé à l'entité User et à l'événement UserCreatedDomainEvent, le systÚme crée automatiquement un gestionnaire d'événements nommé UserCreatedDomainEventHandler.

Structure générée par Lino :

<ProjectName>/
└── src/
    └── Services/
        └── <ServiceName>/
            └── Application/
                ├── <ProjectName>.<ServiceName>.Application.csproj
                └── UseCases/
                    └── Users/
                        ├── Commands/
                        ├── EventHandlers/
                        │   └── Domain/
                        │       └── UserCreatedDomainEventHandler.cs
                        ├── Logging/
                        ├── Queries/
                        └── Resources/

Technical responsibilities

  • Implements IDomainEventHandler and reacts inside the application boundary.
  • Runs internal consistency, logging, tracing, or integration registration work.
  • Uses IOutbox.RegisterIntegrationEvent for reliable messages.
  • Keeps the business change and OutboxMessage in the same transaction.

ÉvĂ©nements d'IntĂ©gration

Les Ă©vĂ©nements d'intĂ©gration sont des messages indiquant qu'un Ă©vĂ©nement important s'est produit et doivent ĂȘtre partagĂ©s avec des systĂšmes externes ou d'autres microservices.

Contrairement aux événements de domaine, l'objectif ici est la communication entre systÚmes et la synchronisation des états.

Quand crĂ©er un ÉvĂ©nement d'IntĂ©gration :

  • Lorsque un changement dans votre systĂšme doit ĂȘtre reflĂ©tĂ© dans un autre systĂšme.
  • Lorsque votre microservice doit publier des changements afin que d'autres microservices puissent rĂ©agir.

Principales différences entre les Domain Events et les Integration Events :

Aspect ÉvĂ©nement de Domaine ÉvĂ©nement d'IntĂ©gration
Public cible Interne Externe
Couplage Faible (interne) Nécessaire (entre systÚmes)
Temps de traitement ImmĂ©diat Peut ĂȘtre asynchrone, avec garanties de livraison
Persistance requise Non obligatoire Oui (pour fiabilité et résilience)

Dans Lino, créer un nouvel événement d'intégration est simple. Vous pouvez exécuter :

lino event new
lino event new --name <EventName> --service <ServiceName> --module <ModuleName> --entity <EntityName>
lino event list --service <ServiceName> --module <ModuleName> --entity <EntityName>

Le assistant CLI demandera :

  • Service – Le service dans lequel l'Ă©vĂ©nement sera créé.
  • Module – Le module dans lequel l'Ă©vĂ©nement sera créé (uniquement pour les services modulaires).
  • EntitĂ© – L'entitĂ© dans laquelle l'Ă©vĂ©nement sera créé / associĂ©.
  • Type d'Ă©vĂ©nement – ÉvĂ©nement de domaine ou Ă©vĂ©nement d'intĂ©gration.
  • Nom de l'Ă©vĂ©nement – Nom utilisĂ© pour l'Ă©vĂ©nement d'intĂ©gration, associĂ© Ă  l'entitĂ©.

Exemple

En créant l'événement d'intégration UserCreated associé à l'entité User, le systÚme crée automatiquement un événement nommé UserCreatedIntegrationEvent. Ce nom indique clairement à toute partie du systÚme consommant l'événement que l'action de création de l'utilisateur a déjà été effectuée.

Structure générée par Lino :

<ProjectName>/
└── src/
    └── Services/
        └── <ServiceName>/
            └── Integration.Events/
                ├── <ProjectName>.<ServiceName>.Integration.Events.csproj
                └── Users/
                    └── UserCreatedIntegrationEvent.cs

Reliable publication

An integration event implements IIntegrationEvent and crosses boundaries between modules, services, or systems. The application registers the event in IOutbox, the Unit of Work persists an OutboxMessage, and a Hangfire worker later publishes it through MassTransit/RabbitMQ.

Gestionnaires d'ÉvĂ©nements d'IntĂ©gration

Un Integration Event Handler est une classe responsable de consommer un ÉvĂ©nement d'IntĂ©gration, gĂ©nĂ©ralement publiĂ© par un autre service ou contexte, puis d'exĂ©cuter des actions spĂ©cifiques dans son propre domaine.

Ces handlers reçoivent les événements via des mécanismes de messagerie (comme RabbitMQ, Kafka, Azure Service Bus, etc.), généralement en combinaison avec le pattern Outbox, qui garantit une livraison fiable et un traitement asynchrone.

Par exemple, lorsqu'un Ă©vĂ©nement UserCreated est publiĂ© par un service d'IdentitĂ©, le handler correspondant dans un autre contexte peut rĂ©agir en envoyant un e-mail de bienvenue ou en appelant des APIs externes. Ces opĂ©rations peuvent ĂȘtre plus lentes ou sujettes Ă  des erreurs, mais comme elles sont traitĂ©es en dehors de la transaction principale, elles ne compromettent pas la cohĂ©rence interne de l'application.

Caractéristiques principales :

  • RĂ©agit aux Ă©vĂ©nements reprĂ©sentant des faits mĂ©tier pertinents pour d'autres contextes.
  • ExĂ©cute des opĂ©rations pouvant ĂȘtre lentes ou externes (ex. : envoi d'e-mails, appels Ă  des APIs).
  • Est traitĂ© de maniĂšre asynchrone et rĂ©siliente, souvent avec des tentatives de reprise et de la surveillance.
  • Garantit que les Ă©checs externes n'affectent pas la transaction d'origine du domaine.
  • Facilite l'intĂ©gration entre les bounded contexts et les systĂšmes distribuĂ©s.

Pour créer un nouveau gestionnaire d'événements d'intégration, il suffit d'exécuter la commande :

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>

L'assistant CLI demandera :

  • Service – Le service dans lequel le gestionnaire d'Ă©vĂ©nements sera créé.
  • Module – Le module dans lequel le gestionnaire d'Ă©vĂ©nements sera créé (uniquement pour les services modulaires).
  • EntitĂ© – L'entitĂ© dans laquelle le gestionnaire d'Ă©vĂ©nements sera créé.
  • Type d'Ă©vĂ©nement – ÉvĂ©nement de domaine ou Ă©vĂ©nement d'intĂ©gration.
  • Service – Le service dans lequel existe l'Ă©vĂ©nement Ă  consommer.
  • Module – Le module dans lequel existe l'Ă©vĂ©nement Ă  consommer (uniquement pour les services modulaires).
  • EntitĂ© – L'entitĂ© dans laquelle existe l'Ă©vĂ©nement Ă  consommer.
  • ÉvĂ©nement – L'Ă©vĂ©nement qui sera consommĂ©.
  • Nom du gestionnaire d'Ă©vĂ©nement – Nom utilisĂ© qui sera associĂ© Ă  l'entitĂ© et Ă  l'Ă©vĂ©nement d'intĂ©gration.

Exemple

En créant le gestionnaire d'événement d'intégration SendEmailOnUserCreated associé à l'entité User et à l'événement UserCreatedIntegrationEvent, le systÚme crée automatiquement un gestionnaire d'événement nommé SendEmailOnUserCreatedIntegrationEventHandler.

Structure générée par Lino :

<ProjectName>/
└── src/
    └── Services/
        └── <ServiceName>/
            └── Application/
                ├── <ProjectName>.<ServiceName>.Application.csproj
                └── UseCases/
                    └── Users/
                        ├── Commands/
                        ├── EventHandlers/
                        │   └── Integration/
                        │       └── SendEmailOnUserCreatedIntegrationEventHandler.cs
                        ├── Logging/
                        ├── Queries/
                        └── Resources/

Operational guidance

  • Implements IIntegrationEventHandler and consumes messages with MassTransit.
  • Does not read the producer database directly.
  • Updates local state, projections, or shadow entities.
  • Should be idempotent because retries and duplicate messages can happen.

Shadow Entities

Une shadow entity est une copie locale, minimale et contrÎlée de données dont le propriétaire se trouve dans un autre module ou service. Elle existe pour réduire le couplage lorsque le consommateur doit consulter, valider ou afficher des données de référence sans accéder directement à la base de données du producteur.

Les Shadow Entities sont utiles lorsque la cohérence éventuelle est acceptable. Le producteur publie un événement d'intégration, le message est persisté dans l'Outbox, le worker le publie sur le bus et le consommateur met à jour sa copie locale au moyen d'un integration event handler.

lino shadow-entity new
lino event new
lino event-handler new

La commande est aussi accessible avec l'alias lino shadow new. Dans le flux interactif, Lino demande le service ou module cible, l'entité source et les propriétés à copier. Cela évite de recréer manuellement le type d'identifiant, le caractÚre obligatoire, la longueur des strings et les références de base, ce qui réduit les incohérences entre source et consommateur.

La shadow entity n'est pas une réplique complÚte. Dans l'exemple d'un module Catalog consommant des données de Tenancy et Security, le catalogue peut ne conserver que Tenant.Id, Tenant.Slug, User.Id, User.Email et User.TenantId. L'entité utilisateur originale contient toujours hash, confirmation, tokens, dates et autres champs internes qui n'appartiennent pas au contexte du catalogue.

Quand l'utiliser

  • Lorsque le consommateur a besoin de quelques champs d'une entitĂ© appartenant Ă  un autre contexte.
  • Lorsque la rĂšgle accepte un lĂ©ger dĂ©lai entre le changement chez le producteur et la mise Ă  jour chez le consommateur.
  • Lorsqu'une lecture directe de la base du producteur crĂ©erait un couplage inappropriĂ© entre modules ou services.
  • Lorsque des grids, filtres, validations locales ou projections ont besoin de donnĂ©es de rĂ©fĂ©rence externes.

Quand l'éviter

  • Lorsque la rĂšgle exige une donnĂ©e mise Ă  jour immĂ©diatement et n'accepte pas la cohĂ©rence Ă©ventuelle.
  • Lorsque la copie locale deviendrait une rĂ©plique complĂšte de l'aggregate original.
  • Lorsque le consommateur commence Ă  dĂ©pendre d'invariants qui appartiennent au producteur.

Flux recommandé

  1. Modélisez la shadow entity avec uniquement les champs nécessaires au consommateur.
  2. Créez ou sélectionnez l'integration event publié par le producteur.
  3. Créez un integration event handler dans le consommateur.
  4. Implémentez un upsert idempotent avec la clé du producteur ou un identifiant naturel.
  5. Ajoutez assez de logs pour diagnostiquer retries, messages dupliqués et échecs permanents.

Synchronisation OrientĂ©e ÉvĂ©nements

Le flux complet combine domaine, application, Unit of Work, Outbox, worker, bus et consommateur. L'objectif est d'éviter les appels directs fragiles entre modules ou services et de permettre retry, audit et retraitement lorsque la publication ou la consommation échoue.

  1. Un aggregate change et enregistre un événement de domaine.
  2. La Unit of Work enregistre le changement dans une transaction.
  3. Un domain handler réagit au fait et enregistre un événement d'intégration dans IOutbox.
  4. La Unit of Work persiste les donnĂ©es mĂ©tier et OutboxMessage dans la mĂȘme transaction.
  5. Un worker Hangfire lit l'Outbox et publie sur le bus via MassTransit/RabbitMQ.
  6. Le consommateur reçoit le message via un IIntegrationEventHandler.
  7. Le consommateur met à jour son modÚle local, shadow entity, projection, e-mail, intégration externe ou autre flux asynchrone.
Domain change -> Domain Event -> Domain Handler -> Integration Event -> Outbox -> Worker -> Event Bus -> Consumer -> Local model

RĂšgles de conception

  • Placez les invariants dans le domaine; utilisez les Ă©vĂ©nements pour les rĂ©actions, pas pour cacher des rĂšgles obligatoires.
  • Enregistrez les integration events dans la mĂȘme transaction que le changement mĂ©tier.
  • Gardez les contrats d'intĂ©gration petits, versionnables et stables.
  • ImplĂ©mentez des consumers idempotents, car retries et messages dupliquĂ©s peuvent se produire.
  • Surveillez les Ă©checs de worker et les messages bloquĂ©s dans l'Outbox.

RĂ©sultat pratique : le producteur n'a pas besoin de connaĂźtre les consommateurs, les consommateurs n'accĂšdent pas Ă  la base du producteur et la communication entre contextes reste rĂ©siliente mĂȘme lorsque le broker ou le consommateur est temporairement indisponible.

Une erreur non gérée est survenue. Rafraîchir 🗙