Trabalhando com eventos
Eventos no Lino modelam fatos que já aconteceram e ajudam a manter aplicações .NET desacopladas entre aggregates, módulos, serviços e integrações assíncronas. Esta página explica eventos de domínio, eventos de integração, seus handlers, shadow entities e o fluxo de Transactional Outbox usado pelo Lino para publicar mensagens confiáveis com Clean Architecture, DDD, MediatR, MassTransit, RabbitMQ, Hangfire e Unit of Work.
Eventos de Domínio
Um evento de domínio representa um fato relevante que já aconteceu dentro do modelo de domínio. No Lino, um evento de domínio gerado é um record selado com sufixo DomainEvent e implementa IDomainEvent, que também é uma notificação do MediatR. O aggregate ou entidade registra esse fato na sua lista interna de eventos por meio de RegisterDomainEvent.
Use um evento de domínio quando uma operação de negócio altera o estado de um aggregate e outras partes da mesma aplicação precisam reagir sem dependência direta. O evento deve descrever o fato de negócio, não a ação técnica que será executada depois.
Quando utilizar um evento de domínio
- Quando uma operação gerar uma mudança significativa no domínio e houver necessidade de acionar outras regras internas.
- Para manter aggregates coesos, evitando chamadas diretas para use cases, módulos, infraestrutura de mensageria ou serviços externos.
- Para iniciar um fluxo de integração por meio de um handler de domínio que registra uma mensagem no Outbox.
- Para tornar efeitos colaterais explícitos, como
UserCreated,VehicleCreated,RolePermissionsChangedouProductPriceChanged.
Não use evento como comando disfarçado. Um comando pede para algo acontecer; um evento declara que algo já aconteceu. Por isso, nomes devem ficar no passado, como UserCreatedDomainEvent ou ProductCreatedDomainEvent.
Criando eventos pelo CLI
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>
O assistente do CLI solicitará:
- Serviço: serviço no qual o evento será criado.
- Módulo: módulo em que o evento será criado, quando o serviço for modular.
- Entidade: entidade ou aggregate ao qual o evento será associado.
- Tipo de evento: evento de domínio ou evento de integração.
- Nome do evento: nome do fato ocorrido, normalmente no passado.
- Propriedades: dados mínimos que o handler precisará para reagir ao fato.
Fluxo de execução
- Um command handler chama um método em um aggregate ou entidade.
- A entidade altera estado e registra o evento de domínio internamente.
- O command handler salva por Unit of Work, preferencialmente com
SaveChangesInTransactionAsyncquando há eventos envolvidos. - A Unit of Work persiste as mudanças do aggregate e publica os eventos coletados por MediatR.
- Domain Event handlers executam reações internas e podem registrar eventos de integração no Outbox.
Exemplo
Criando o evento de domínio UserCreated associado à entidade User, o sistema cria automaticamente UserCreatedDomainEvent. Esse nome deixa claro que a criação do usuário já foi concluída.
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Domain/
├── <ProjectName>.<ServiceName>.Domain.csproj
└── Aggregates/
└── Users/
├── User.cs
├── Errors/
├── Events/
│ └── UserCreatedDomainEvent.cs
├── Repositories/
└── Resources/
Manipuladores de Eventos de Domínio
Um Domain Event Handler reage a um evento de domínio dentro da mesma fronteira da aplicação. No Lino, handlers de domínio implementam IDomainEventHandler e são executados por MediatR quando a Unit of Work publica eventos coletados das entidades rastreadas.
O handler é o lugar correto para regras que devem acontecer porque um fato de domínio ocorreu, mas que não deveriam ficar embutidas no aggregate. Isso mantém o aggregate focado em estado e invariantes, enquanto as reações da aplicação ficam explícitas e testáveis.
Por exemplo, após a criação de um User, pode ser necessário atualizar estatísticas, registrar log interno, notificar outro aggregate ou preparar um evento de integração. Essas ações fazem sentido dentro da aplicação e podem ocorrer de forma síncrona quando dependem de consistência imediata.
Operações lentas ou instáveis, como envio de e-mails, chamadas a APIs externas ou publicação direta em broker, não devem bloquear a transação de domínio. Nesses casos, um handler de domínio pode mapear os dados necessários para UserCreatedIntegrationEvent e chamar IOutbox.RegisterIntegrationEvent. A integração será persistida como OutboxMessage pela mesma Unit of Work.
Responsabilidades recomendadas
- Reagir a um fato de domínio já ocorrido sem alterar o significado do evento.
- Executar regras internas de consistência, logging, tracing, atualizações recursivas de domínio ou registro de integração.
- Mover trabalho externo lento ou não confiável para eventos de integração.
- Usar a mesma transação quando registrar mensagens no Outbox ou depender do estado persistido do aggregate.
Criando handlers pelo CLI
lino event-handler new
O assistente do CLI solicitará:
- Serviço: serviço em que o handler será criado.
- Módulo: módulo em que o handler será criado, quando aplicável.
- Entidade: entidade/use case ao qual o handler será associado.
- Tipo de evento: evento de domínio ou evento de integração.
- Evento: evento que será consumido, como
UserCreatedDomainEvent. - Nome do handler: nome que descreve a reação, como
PublishUserCreated.
Passo a passo
- Escolha a entidade ou use case que levanta o evento de domínio.
- Selecione Domain Event como tipo do handler.
- Selecione o evento existente, por exemplo
UserCreatedDomainEvent. - Nomeie o handler pelo propósito, como
UserCreatedouPublishUserCreated. - Implemente o método
Handlegerado com uma reação clara.
Exemplo de estrutura gerada
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Application/
├── <ProjectName>.<ServiceName>.Application.csproj
└── UseCases/
└── Users/
├── Commands/
├── EventHandlers/
│ └── Domain/
│ └── UserCreatedDomainEventHandler.cs
├── Logging/
├── Queries/
└── Resources/
Eventos de Integração
Um evento de integração é um contrato de mensagem usado para comunicar um fato de negócio para outro módulo, serviço ou sistema. No Lino, eventos de integração gerados são records selados com sufixo IntegrationEvent e implementam IIntegrationEvent.
Eventos de integração diferem dos eventos de domínio porque cruzam uma fronteira. Um evento de domínio descreve o que aconteceu dentro do domínio. Um evento de integração carrega apenas os dados que outro contexto tem permissão e necessidade de conhecer; por isso, o payload deve ser intencional e estável, não uma cópia completa do aggregate.
O Lino publica eventos de integração com confiabilidade usando o padrão Transactional Outbox. A aplicação registra o evento em IOutbox; a Unit of Work converte o envelope em OutboxMessage e o armazena na mesma transação da mudança de negócio; depois, um worker lê mensagens pendentes e publica por IEventBus, implementado com MassTransit e RabbitMQ.
Domain Event vs. integration event
| Aspecto | Evento de domínio | Evento de integração |
|---|---|---|
| Escopo | Dentro da aplicação ou domínio | Entre módulos, serviços ou sistemas |
| Contrato | Notificação interna via IDomainEvent | Contrato de mensagem via IIntegrationEvent |
| Entrega | Publicado pela Unit of Work via MediatR | Persistido no Outbox e publicado depois pelo worker/event bus |
| Payload | Pode carregar dados internos do aggregate | Deve carregar apenas campos necessários aos consumidores |
| Falhas | Afetam o fluxo transacional interno | Devem permitir retry, auditoria e reprocessamento |
Criando eventos de integração
lino event new lino event new --name <EventName> --service <ServiceName> --module <ModuleName> --entity <EntityName> lino event list --service <ServiceName> --module <ModuleName> --entity <EntityName>
Escolha Integration Event no assistente. O Lino pergunta qual entidade possui o evento e quais propriedades devem entrar na mensagem. Primary keys são incluídas para que consumidores possam correlacionar ou fazer upsert de dados locais.
Fluxo de publicação confiável
- Um use case salva uma mudança de negócio em uma transação.
- Um handler de domínio cria um evento de integração e o registra em
IOutbox. - A Unit of Work persiste um
OutboxMessagecom nome do evento, tipo assembly-qualified, conteúdo serializado, status de retry e data opcional de agendamento. - O worker Hangfire lê mensagens disponíveis em lotes e publica por MassTransit/RabbitMQ.
- O handler de integração consumidor recebe a mensagem e atualiza seu próprio modelo local.
Exemplo de estrutura gerada
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Integration.Events/
├── <ProjectName>.<ServiceName>.Integration.Events.csproj
└── Users/
└── UserCreatedIntegrationEvent.cs
Manipuladores de Eventos de Integração
Um Integration Event Handler consome um evento de integração publicado pela infraestrutura de mensageria. No Lino, handlers de integração gerados implementam IIntegrationEventHandler e são consumers do MassTransit, recebendo um ConsumeContext com a mensagem e o cancellation token.
Use integration handlers quando um serviço ou módulo precisa atualizar seu próprio estado com base em um fato ocorrido em outro lugar. O handler não deve ler diretamente o banco de outro módulo. Ele consome o contrato da mensagem e executa uma escrita local, frequentemente criando ou atualizando uma cópia local ou shadow entity com apenas os campos necessários para aquele contexto.
Por exemplo, Shipment pode receber eventos de vehicle e driver vindos de Fleet e armazenar registros locais associados. O mesmo padrão vale para Catalog consumir TenantCreatedIntegrationEvent e UserCreatedIntegrationEvent para manter cópias mínimas necessárias ao seu contexto.
Criando handlers de integração
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>
O CLI pergunta onde o handler será criado e qual evento de integração será consumido. Para eventos de outro módulo ou serviço, o Lino também adiciona a referência necessária ao projeto de integration events para que o handler use o contrato da mensagem.
Passo a passo
- Escolha serviço, módulo e entidade consumidores onde a reação local será implementada.
- Selecione Integration Event como tipo do handler.
- Escolha serviço, módulo e entidade produtores que possuem o contrato do evento.
- Selecione o evento a consumir, como
TenantCreatedIntegrationEventouUserCreatedIntegrationEvent. - Nomeie o handler pela intenção, como
CreateUserShadowOnUserCreatedouSendEmailOnUserCreated. - Implemente lógica idempotente sempre que possível, porque mensagens podem ser reprocessadas depois de falhas.
Orientações operacionais
- Mantenha handlers focados em uma reação. Um evento pode ter múltiplos handlers.
- Persista mudanças pela Unit of Work e pelos repositories do contexto consumidor.
- Não leia diretamente o banco do produtor.
- Permita que exceções subam quando o processamento não puder ser concluído; o fluxo Outbox/worker registra falhas e tentativas.
- Use logs e traces gerados pelo Lino para inspecionar a cadeia do domain event ao integration event e ao processamento consumidor.
Exemplo de estrutura gerada
<ProjectName>/
└── src/
└── Services/
└── <ServiceName>/
└── Application/
├── <ProjectName>.<ServiceName>.Application.csproj
└── UseCases/
└── Users/
├── Commands/
├── EventHandlers/
│ └── Integration/
│ └── SendEmailOnUserCreatedIntegrationEventHandler.cs
├── Logging/
├── Queries/
└── Resources/
Shadow Entities
Uma shadow entity é uma cópia local, mínima e controlada de dados cujo dono está em outro módulo ou serviço. Ela existe para reduzir acoplamento quando o consumidor precisa consultar, validar ou exibir dados de referência sem acessar diretamente o banco do produtor.
Shadow Entities são úteis quando consistência eventual é aceitável. O produtor publica um evento de integração, a mensagem é persistida no Outbox, o worker publica no barramento e o consumidor atualiza sua cópia local por um integration event handler.
lino shadow-entity new lino event new lino event-handler new
O comando também pode ser acessado pelo alias lino shadow new. No fluxo interativo, o Lino pergunta o serviço ou módulo de destino, a entidade de origem e quais propriedades devem ser copiadas. Isso evita recriar manualmente tipo de identificador, obrigatoriedade, tamanho de string e referências básicas, reduzindo inconsistências entre origem e consumidor.
A shadow entity não é uma réplica completa. No exemplo de um módulo Catalog consumindo dados de Tenancy e Security, o catálogo pode manter apenas Tenant.Id, Tenant.Slug, User.Id, User.Email e User.TenantId. A entidade original de usuário continua contendo hash, confirmação, tokens, datas e outros campos internos que não pertencem ao contexto do catálogo.
Quando usar
- Quando o consumidor precisa de poucos campos de uma entidade pertencente a outro contexto.
- Quando a regra aceita atraso pequeno entre a mudança no produtor e a atualização no consumidor.
- Quando leitura direta do banco produtor criaria acoplamento indevido entre módulos ou serviços.
- Quando grids, filtros, validações locais ou projeções precisam de dados de referência externos.
Quando evitar
- Quando a regra exige dado atualizado no mesmo instante e não aceita consistência eventual.
- Quando a cópia local viraria uma réplica completa do aggregate original.
- Quando o consumidor começa a depender de invariantes que pertencem ao produtor.
Fluxo recomendado
- Modele a shadow entity com apenas os campos necessários ao consumidor.
- Crie ou selecione o integration event publicado pelo produtor.
- Crie um integration event handler no consumidor.
- Implemente upsert idempotente usando chave do produtor ou identificador natural.
- Registre logs suficientes para diagnosticar retries, mensagens duplicadas e falhas permanentes.
Sincronização Orientada por Eventos
O fluxo completo combina domínio, aplicação, Unit of Work, Outbox, worker, barramento e consumidor. O objetivo é evitar chamadas diretas frágeis entre módulos ou serviços e permitir retry, auditoria e reprocessamento quando a publicação ou o consumo falhar.
- Um aggregate muda e registra um evento de domínio.
- A Unit of Work salva a mudança dentro de uma transação.
- Um handler de domínio reage ao fato e registra um evento de integração em
IOutbox. - A Unit of Work persiste dados de negócio e
OutboxMessagena mesma transação. - Um worker Hangfire lê a Outbox e publica no barramento via MassTransit/RabbitMQ.
- O consumidor recebe a mensagem por um
IIntegrationEventHandler. - O consumidor atualiza seu modelo local, shadow entity, projeção, e-mail, integração externa ou outro fluxo assíncrono.
Domain change -> Domain Event -> Domain Handler -> Integration Event -> Outbox -> Worker -> Event Bus -> Consumer -> Local model
Regras de desenho
- Coloque invariantes no domínio; use eventos para reações, não para esconder regras obrigatórias.
- Registre integration events dentro da mesma transação da mudança de negócio.
- Mantenha contratos de integração pequenos, versionáveis e estáveis.
- Implemente consumers idempotentes, pois retries e mensagens duplicadas podem acontecer.
- Monitore falhas de worker e mensagens paradas no Outbox.
Resultado prático: o produtor não precisa conhecer consumidores, consumidores não acessam o banco do produtor e a comunicação entre contextos continua resiliente mesmo quando o broker ou o consumidor está temporariamente indisponível.
