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, RolePermissionsChanged ou ProductPriceChanged.

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

  1. Um command handler chama um método em um aggregate ou entidade.
  2. A entidade altera estado e registra o evento de domínio internamente.
  3. O command handler salva por Unit of Work, preferencialmente com SaveChangesInTransactionAsync quando há eventos envolvidos.
  4. A Unit of Work persiste as mudanças do aggregate e publica os eventos coletados por MediatR.
  5. 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

  1. Escolha a entidade ou use case que levanta o evento de domínio.
  2. Selecione Domain Event como tipo do handler.
  3. Selecione o evento existente, por exemplo UserCreatedDomainEvent.
  4. Nomeie o handler pelo propósito, como UserCreated ou PublishUserCreated.
  5. Implemente o método Handle gerado 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

AspectoEvento de domínioEvento de integração
EscopoDentro da aplicação ou domínioEntre módulos, serviços ou sistemas
ContratoNotificação interna via IDomainEventContrato de mensagem via IIntegrationEvent
EntregaPublicado pela Unit of Work via MediatRPersistido no Outbox e publicado depois pelo worker/event bus
PayloadPode carregar dados internos do aggregateDeve carregar apenas campos necessários aos consumidores
FalhasAfetam o fluxo transacional internoDevem 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

  1. Um use case salva uma mudança de negócio em uma transação.
  2. Um handler de domínio cria um evento de integração e o registra em IOutbox.
  3. A Unit of Work persiste um OutboxMessage com nome do evento, tipo assembly-qualified, conteúdo serializado, status de retry e data opcional de agendamento.
  4. O worker Hangfire lê mensagens disponíveis em lotes e publica por MassTransit/RabbitMQ.
  5. 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

  1. Escolha serviço, módulo e entidade consumidores onde a reação local será implementada.
  2. Selecione Integration Event como tipo do handler.
  3. Escolha serviço, módulo e entidade produtores que possuem o contrato do evento.
  4. Selecione o evento a consumir, como TenantCreatedIntegrationEvent ou UserCreatedIntegrationEvent.
  5. Nomeie o handler pela intenção, como CreateUserShadowOnUserCreated ou SendEmailOnUserCreated.
  6. 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

  1. Modele a shadow entity com apenas os campos necessários ao consumidor.
  2. Crie ou selecione o integration event publicado pelo produtor.
  3. Crie um integration event handler no consumidor.
  4. Implemente upsert idempotente usando chave do produtor ou identificador natural.
  5. 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.

  1. Um aggregate muda e registra um evento de domínio.
  2. A Unit of Work salva a mudança dentro de uma transação.
  3. Um handler de domínio reage ao fato e registra um evento de integração em IOutbox.
  4. A Unit of Work persiste dados de negócio e OutboxMessage na mesma transação.
  5. Um worker Hangfire lê a Outbox e publica no barramento via MassTransit/RabbitMQ.
  6. O consumidor recebe a mensagem por um IIntegrationEventHandler.
  7. 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.

Ocorreu um erro não tratado. Recarregar 🗙