Event Sourcing 是将状态变更全部建模为不可变事件流,系统状态通过重放事件得到;事件须不可变、有序、持久化且带完整上下文;Apply 方法必须纯函数式;快照是必需的性能与正确性保障机制。

Event Sourcing 是什么,它不等于“加一堆 event”
Event Sourcing 的核心不是给类加 public event EventHandler,而是把状态变更**全部建模为不可变事件流**,系统当前状态由重放这些事件得到。你写的是“发生了什么”,而不是“现在是什么”。如果误以为只是用 event 关键字发通知,后续做快照、回溯、调试时会发现根本没法还原历史——因为那些事件没被持久化、没带完整上下文、没按时间顺序严格排序。
如何定义和存储事件(C# 实操要点)
事件是纯数据容器,必须满足:不可变、可序列化、带唯一 ID 和时间戳。不要在事件里存引用类型或业务逻辑。
- 用
record(C# 9+)定义事件,比如public record OrderPlaced(Guid OrderId, decimal Amount, DateTime OccurredAt); - 存储时必须保证顺序和幂等性:推荐用支持有序追加的存储,如 PostgreSQL 的表(带
sequence或timestamp排序字段)、SQL Server 的IDENTITY列,或专用事件存储如 EventStoreDB - 避免用 Entity Framework 直接映射事件——它默认按主键查,而事件查询主场景是“按聚合 ID + 版本号范围读”,EF 容易生成低效 SQL
如何从事件重建聚合根状态(Apply 方法陷阱)
聚合根的 Apply 方法负责将事件“应用”到内存状态,但常见错误是让它承担副作用(如发邮件、调外部 API),或者让 Apply 修改非当前聚合的状态。
-
Apply必须是纯函数式:只修改this的字段,不访问数据库、不抛异常(除非校验失败)、不触发其他事件 - 加载聚合时,先 new 出空实例,再按版本升序依次调用
Apply(event),顺序错一帧,状态就错到底 - 别在
Apply里做复杂计算——比如“计算累计积分”,应改为事件自带CumulativePoints字段,否则重放时浮点误差或逻辑变更会导致状态不一致
为什么需要快照(Snapshot),以及怎么安全地用
当一个聚合产生上千个事件,每次重建都重放太慢。快照就是某个版本的状态“照片”,但它不是可选优化,而是实际生产中必须处理的环节。
- 快照应在聚合版本达到阈值(如 100)后自动创建,保存聚合 ID、快照版本(即该快照对应最后那个事件的版本号)、序列化后的状态
- 加载时,先查最新快照,再查该快照版本之后的所有事件,合并重放——注意:快照本身不能替代事件,事件仍是唯一真相源
- 快照格式必须向后兼容:如果改了聚合字段,旧快照反序列化会失败。建议用显式 DTO(如
OrderSnapshotV1)而非直接序列化聚合根
最常被忽略的是事件版本与快照版本的对齐逻辑——差一个版本,就可能漏掉一次状态变更。这事关数据正确性,不是性能问题。










