处理事件
在现代系统中,遵循诸如 DDD(领域驱动设计)和 Clean Architecture(整洁架构)等架构最佳实践时,事件是用于建模状态变化以及组件或系统之间异步通信的基本机制。 事件表示在应用程序域中已经发生的事情,并且可能对系统的其他部分或外部服务有兴趣。 补充: This section also covers shadow entities and the Transactional Outbox flow with DDD, MediatR, MassTransit, RabbitMQ, Hangfire, and Unit of Work for reliable messaging.
领域事件
领域事件表示在应用程序上下文中发生的重要事实。 它们在系统内部生成和消费,允许状态变化以解耦的方式传播,也就是说,对象之间无需直接相互了解。
何时使用领域事件?
- 当某个操作在领域中产生重大变化,并且需要通知或触发系统的其他部分时。
- 为保持领域模型的内聚性,使不同的流程能够触发而无需在模块或服务之间创建直接依赖。
在 Lino 中,创建新的领域事件非常简单。您可以执行:
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>
CLI 向导将要求提供:
- 服务 – 将要创建事件的服务。
- 模块 – 将要创建事件的模块(仅适用于模块化服务)。
- 实体 – 事件将创建/关联的实体。
- 事件类型 – 领域事件或集成事件。
- 事件名称 – 在领域中使用的名称,与实体关联。
示例
创建与 User 实体关联的领域事件 UserCreated 时,
系统会自动创建名为 UserCreatedDomainEvent 的事件。
该名称清楚地表明,消费该事件的系统部分,用户创建操作已完成。
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.
领域事件处理器
Domain Event Handler 是一个类,负责响应 领域事件,执行与应用程序内部状态相关的操作,并始终在相同的事务上下文中运行。
这些处理器的主要目的是保持系统的内聚性和松耦合,使得可以在不增加实体或聚合核心逻辑负担的情况下应用额外规则。
例如,在创建 User 后,可能需要更新统计数据、生成内部日志或通知其他聚合。
这些操作在领域内是合理的,可以同步执行,从而保证即时一致性。
但是,依赖外部资源的操作——例如发送电子邮件或调用第三方 API——不应直接从领域事件中执行,因为这会将事务绑定到缓慢或不稳定的任务。 在这种情况下,领域可以生成一个集成事件(记录在 Outbox 中),稍后以异步且具弹性的方式处理。
主要特性:
- 响应事件,但绝不修改事件。
- 仅执行与领域一致性相关的进程内操作。
- 确保所有操作在同一事务中运行。
要创建新的领域事件处理器,只需运行以下命令:
lino event-handler new
CLI 向导将要求提供:
- 服务 – 创建事件处理器的服务。
- 模块 – 创建事件处理器的模块(仅在模块化服务中)。
- 实体 – 创建事件处理器的实体。
- 事件类型 – 领域事件或集成事件。
- 事件 – 将被消费的事件。
- 事件处理器名称 – 将与实体和领域事件关联的名称。
示例
创建与实体 User 和事件 UserCreatedDomainEvent 关联的领域事件处理器 UserCreated,系统将自动创建名为 UserCreatedDomainEventHandler 的事件处理器。
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.
集成事件
集成事件是指示发生了重要事件的消息,需要与外部系统或其他微服务共享。
与领域事件不同,这里的目标是系统间通信和状态同步。
何时创建集成事件:
- 当系统中的更改需要反映到其他系统时。
- 当您的微服务需要发布更改,以便其他微服务做出响应时。
领域事件与集成事件的主要区别:
| 方面 | 领域事件 | 集成事件 |
|---|---|---|
| 目标受众 | 内部 | 外部 |
| 耦合度 | 低(内部) | 必要(系统间) |
| 处理时间 | 即时 | 可以异步,并提供交付保证 |
| 持久化需求 | 非必须 | 是(为了可靠性和弹性) |
在 Lino 中,创建新的集成事件很简单。 您可以执行:
lino event new lino event new --name <EventName> --service <ServiceName> --module <ModuleName> --entity <EntityName> lino event list --service <ServiceName> --module <ModuleName> --entity <EntityName>
CLI 助手将提示:
- 服务 – 创建事件的服务。
- 模块 – 创建事件的模块(仅适用于模块化服务)。
- 实体 – 创建/关联事件的实体。
- 事件类型 – 领域事件或集成事件。
- 事件名称 – 集成事件的名称,与实体关联。
示例
创建与实体 User 关联的集成事件 UserCreated 时,系统会自动生成名为 UserCreatedIntegrationEvent 的事件。
此名称清楚地告诉系统中消费该事件的任何部分,用户创建操作已完成。
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.
集成事件处理器
Integration Event Handler 是一个类,负责消费通常由其他服务或上下文发布的 Integration Event, 并在自身的领域内执行特定操作。
这些处理器通过消息机制(如 RabbitMQ、Kafka、Azure Service Bus 等)接收事件, 通常与 Outbox 模式结合使用,以确保可靠的投递和异步处理。
例如,当 Identity 服务发布 UserCreated 事件时,另一个上下文中的对应处理器可以通过发送欢迎邮件或调用外部 API 来响应。
这些操作可能较慢或容易失败,但由于它们在主事务之外处理,因此不会影响应用的内部一致性。
主要特性:
- 响应对其他上下文具有业务相关性的事件。
- 执行可能较慢或外部的操作(例如:发送邮件、调用 API)。
- 以异步和弹性的方式处理,通常伴随重试和监控。
- 确保外部失败不会影响原始领域事务。
- 便于有界上下文和分布式系统之间的集成。
要创建新的集成事件处理器,只需运行以下命令:
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>
CLI 向导将请求以下信息:
- 服务 – 创建事件处理器的服务。
- 模块 – 创建事件处理器的模块(仅限模块化服务)。
- 实体 – 创建事件处理器的实体。
- 事件类型 – 域事件或集成事件。
- 服务 – 要消费的事件所在的服务。
- 模块 – 要消费的事件所在的模块(仅限模块化服务)。
- 实体 – 要消费的事件所在的实体。
- 事件 – 将被消费的事件。
- 事件处理器名称 – 将关联到实体和集成事件的名称。
示例
创建与 User 实体及 UserCreatedIntegrationEvent 事件关联的集成事件处理器
SendEmailOnUserCreated 时,系统会自动创建一个名为 SendEmailOnUserCreatedIntegrationEventHandler 的处理器。
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
shadow entity 是一份本地、最小且受控的数据副本,其所有者位于另一个模块或服务中。当 consumer 需要查询、校验或展示参考数据,但又不应直接访问 producer 数据库时,它用于降低耦合。
当 eventual consistency 可以接受时,Shadow Entities 很有用。producer 发布 integration event,消息被持久化到 Outbox,worker 将其发布到 bus,consumer 再通过 integration event handler 更新本地副本。
lino shadow-entity new lino event new lino event-handler new
该命令也可以通过 alias lino shadow new 访问。在交互流程中,Lino 会询问目标服务或模块、源 entity,以及应复制哪些 properties。这可以避免手动重新创建 identifier type、必填性、string 长度和基础 references,从而减少 source 与 consumer 之间的不一致。
shadow entity 不是完整副本。以 Catalog 模块消费 Tenancy 和 Security 数据为例,catalog 可以只保留 Tenant.Id、Tenant.Slug、User.Id、User.Email 和 User.TenantId。原始 user entity 仍然包含 hash、confirmation、tokens、dates 以及其他不属于 catalog context 的内部字段。
何时使用
- 当 consumer 只需要另一个 context 所拥有 entity 的少数字段时。
- 当业务规则可以接受 producer 变更与 consumer 更新之间的小延迟时。
- 当直接读取 producer 数据库会在模块或服务之间造成不恰当耦合时。
- 当 grids、filters、本地 validations 或 projections 需要外部参考数据时。
何时避免
- 当规则要求数据立即更新且不接受 eventual consistency 时。
- 当本地副本会变成原始 aggregate 的完整副本时。
- 当 consumer 开始依赖属于 producer 的 invariants 时。
推荐流程
- 仅使用 consumer 所需字段对 shadow entity 建模。
- 创建或选择 producer 发布的 integration event。
- 在 consumer 中创建 integration event handler。
- 使用 producer key 或 natural identifier 实现 idempotent upsert。
- 记录足够的 logs,以诊断 retries、重复消息和永久失败。
事件驱动同步
完整流程结合 domain、application、Unit of Work、Outbox、worker、bus 和 consumer。目标是避免模块或服务之间脆弱的直接调用,并在发布或消费失败时支持 retry、audit 和 reprocessing。
- aggregate 发生变化并注册 domain event。
- Unit of Work 在 transaction 中保存该变更。
- domain handler 对该事实作出反应,并在
IOutbox中注册 integration event。 - Unit of Work 在同一 transaction 中持久化 business data 和
OutboxMessage。 - Hangfire worker 读取 Outbox,并通过 MassTransit/RabbitMQ 发布到 bus。
- consumer 通过
IIntegrationEventHandler接收消息。 - consumer 更新其 local model、shadow entity、projection、email、external integration 或其他 asynchronous flow。
Domain change -> Domain Event -> Domain Handler -> Integration Event -> Outbox -> Worker -> Event Bus -> Consumer -> Local model
设计规则
- 将 invariants 放在 domain 中;使用 events 表达 reactions,而不是隐藏强制规则。
- 在与 business change 相同的 transaction 中注册 integration events。
- 保持 integration contracts 小型、versionable 且稳定。
- 实现 idempotent consumers,因为 retries 和重复消息可能发生。
- 监控 worker failures 以及停留在 Outbox 中的 messages。
实际结果:producer 不需要知道 consumers,consumers 不访问 producer 数据库,并且即使 broker 或 consumer 暂时不可用,context 之间的通信仍保持 resilient。
