Skip to content
bitzorcas
中EN

Reference

事件抽象层

区分领域事件、集成事件与通知端口,并说明事件收集、主题解析和 Null 适配器的运行边界。

Last updated

BitzOrcas 用不同抽象表达三种意图:聚合内部发生了什么、模块之间承诺发布什么、通知系统需要投递什么。把它们都叫“事件”容易掩盖事务和兼容性差异。

三类契约

契约主要边界作用
IDomainEvent / DomainEventDomain记录聚合状态变化事实,不直接跨模块
IIntegrationEvent 与 owner-local Contracts 类型模块契约跨模块或跨进程的稳定消息契约
INotificationPublisher / NotificationMessageApplication port以 code 和参数请求通知发布,不依赖 CAP

集成事件放在拥有该事实的模块 Contracts 项目中,例如 Identity、Tickets 或 Authorization,而不是集中到一个全局 SaaS.Contracts 包。消费者只依赖发布者的契约边界。

生命周期与事务边界

Notification/Integration PortDomain-event DispatchTransaction PipelineCommand HandlerAggregateNotification/Integration PortDomain-event DispatchTransaction PipelineCommand HandlerAggregate当前自动桥接为提交后 best-effort执行业务行为并 RaiseTrack aggregate在事务中执行提交成功后返回CollectAndClear按已登记 topic 发布

这张图解释自动领域事件桥接,而不是事务内显式集成事件发布。两条路径可以共存,但必须按业务是否需要原子 Outbox 选择,不能用同一个“事件已发布”描述掩盖差异。

领域事件生命周期

聚合通过 Entity<TId>.Raise(IDomainEvent) 保存领域事件。Handler 必须把相关实体交给 scoped IDomainEventCollector.Track(entity);收集器用强类型闭包读取和清空事件,不做运行时反射扫描。

note.Raise(new NoteCreatedDomainEvent(note.Id, note.Title));
domainEvents.Track(note);

一个完整聚合行为会在状态改变成功后抛出事实:

public Result Rename(NoteTitle title, IAppClock clock)
{
// 先保护不变量;失败时既不修改状态,也不产生事件。
if (title == Title)
return Result.Failure(NoteErrors.TitleUnchanged);
Title = title;
ModifyTime = clock.UtcNow;
// 事件只携带稳定事实,不把整个聚合或 ORM 状态交给消费者。
Raise(new NoteRenamedDomainEvent(Id, TenantId, title.Value));
return Result.Success();
}

Handler 保存并 Track 同一个聚合实例。若只 Raise 而未 Track,收集器无法发现事件;若 Restore 时调用产生事件的公共行为,则可能错误重放历史事实。

当前 DomainEventDispatchPipelineBehavior 只处理成功的 Command。它在事务管道返回后收集事件,通过 IIntegrationTopicRegistry 查找编译期登记的主题,再调用 INotificationPublisher。

没有主题映射的领域事件会被跳过;这是允许纯领域事件保留在模块内部。需要跨模块传播时,应显式定义稳定集成契约,而不是依赖 CLR 类型名碰巧一致。

类型化集成事件端口

IIntegrationEventPublisher<TEvent> 让 Application 发布具体契约,而不依赖 ICapPublisher。生产适配器 CapIntegrationEventPublisher<TEvent> 优先使用 [IntegrationTopic] 指定的主题,未指定时回退到类型全名。

显式主题更适合长期契约:它不会因为命名空间重构悄悄改变。事件至少应包含稳定事件 ID、发生时间、租户标识和消费者真正需要的业务数据;不要把持久化实体直接序列化出去。

[IntegrationTopic(Topic)]
public sealed record TicketOpenedIntegrationEvent(
// EventId、TenantId 与发生时间支撑幂等、隔离和链路排错。
Guid EventId,
string TenantId,
string TicketId,
string Subject,
DateTimeOffset OccurredAt)
{
// Topic 是跨版本运维契约,不随 CLR 命名空间重构。
public const string Topic = "tickets.ticket.opened.v1";
}

公开载荷只包含消费者需要的稳定标量。新增可选字段通常兼容;删除字段、改变含义或重用枚举序号属于破坏性变更,应发布新版本并安排并行迁移。

通知端口

INotificationPublisher 接收 NotificationMessage(Code, Parameters)。CapNotificationPublisher 把 Code 作为 CAP topic 发布参数字典。它适用于模板化通知,不应替代强类型业务集成事件。

本地或最小组合可能注册 NullNotificationPublisher、NullIntegrationEventPublisher<T>。Null 适配器用于可选能力和测试隔离;若某条商业流程依赖消息交付,生产就绪检查必须拒绝这种组合,而不能静默接受丢消息。

选择指南

  • 只描述聚合内部事实:领域事件。
  • 需要其他模块可靠消费:owner-local 强类型集成事件。
  • 请求通知模板或渠道投递:通知端口。
  • 需要与业务写原子提交:在 CAP 感知事务中发布,不依赖提交后 best-effort 桥接。

失败语义对照

路径失败发生时的业务状态调用方应做什么
领域行为返回失败状态与事件都不产生返回类型化 Result
事务内集成发布失败业务写与 Outbox 一起回滚保留异常,让事务与重试策略处理
提交后自动桥接失败业务已经提交告警、审计并按业务设计补偿
Null 通知端口取决于能力是否可选可选能力显式降级;必需能力启动失败
消费者失败发布方状态不回滚消费者抛出,CAP 重试并记录失败消息

测试清单

  • 聚合失败时不修改状态、不收集领域事件。
  • 成功行为只 Raise 一次,Restore 不 Raise。
  • Handler Track 正确聚合,成功后收集器会 Clear。
  • 无 topic 的内部事件不跨模块发布。
  • 显式 topic 与模块治理声明、运行时订阅保持一致。
  • 事务内发布失败会回滚业务行与 Outbox。
  • 提交后桥接失败不会伪装成业务回滚,并产生可观测告警。
  • Null 适配器只出现在明确允许的 Edition 与环境。

源码入口

领域事件基类与收集方法位于 BitzOrcas.Domain/Entities;收集器、主题注册表和管线位于 BitzOrcas.Application;CAP 发布适配器位于 BitzOrcas.Infrastructure.Outbox.Cap/Events(独立包);IIntegrationEvent 本体在 BitzOrcas.Modularity.Governance。修改任一契约时,应同时核对发布方契约测试、消费者契约测试与商业 GA 门禁。

传输、Outbox 和消费语义见事件构建块。

100%

滚轮或按钮缩放 · 放大后拖动画面 · 双击切换 100% / 200%