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
- The aggregate registers an
IDomainEventwithRegisterDomainEvent. - The command handler saves through
IUnitOfWork, preferablySaveChangesInTransactionAsyncwhen events are involved. - The Unit of Work persists changes and publishes events through MediatR.
- 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
IDomainEventHandlerand reacts inside the application boundary. - Runs internal consistency, logging, tracing, or integration registration work.
- Uses
IOutbox.RegisterIntegrationEventfor reliable messages. - Keeps the business change and
OutboxMessagein 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
IIntegrationEventHandlerand 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é
- Modélisez la shadow entity avec uniquement les champs nécessaires au consommateur.
- Créez ou sélectionnez l'integration event publié par le producteur.
- Créez un integration event handler dans le consommateur.
- Implémentez un upsert idempotent avec la clé du producteur ou un identifiant naturel.
- 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.
- Un aggregate change et enregistre un événement de domaine.
- La Unit of Work enregistre le changement dans une transaction.
- Un domain handler réagit au fait et enregistre un événement d'intégration dans
IOutbox. - La Unit of Work persiste les données métier et
OutboxMessagedans la mĂȘme transaction. - Un worker Hangfire lit l'Outbox et publie sur le bus via MassTransit/RabbitMQ.
- Le consommateur reçoit le message via un
IIntegrationEventHandler. - 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.
