事件溯源是用不可变事件序列替代直接更新状态的状态管理哲学,要求事件追加写入、带聚合ID和版本号、可重放重建状态,而非仅用C#的public event实现。

事件溯源(Event Sourcing)不是“给类加个 event 就行了”,它是一套状态管理哲学,核心是**用不可变事件序列替代直接更新状态**。如果你正打算在 C# 项目里落地事件溯源,先确认两件事:你是否真的需要可审计、可重放、支持最终一致性的业务模型;以及你是否愿意承担重建聚合、处理快照、应对并发冲突的额外复杂度。
为什么不能直接用 public event EventHandler<ordershippedevent></ordershippedevent>?
这是最常踩的坑:把 .NET 的语言级 event 关键字和领域级“事件溯源”混为一谈。前者只是委托封装的发布-订阅机制,用于 UI 响应或模块解耦;后者是持久化、有序、带版本的业务事实日志。用 event 发一次 OrderShippedEvent,不等于把它写进了 EventStore,也不代表能靠它重建订单状态。
-
event是内存中的一次通知,无持久化、无顺序保证、无版本控制 - 事件溯源要求每个事件必须:
追加写入存储、带聚合根 ID 和版本号、可被重放以重建任意时刻状态 - 常见误操作:在
OnOrderShipped()里只触发OrderShipped事件,却忘了调用_eventStore.AppendAsync(aggregateId, new OrderShippedEvent(...), expectedVersion)
如何正确建模一个可溯源的订单聚合?
关键不在“怎么写事件类”,而在“怎么约束状态变更入口”。C# 中典型做法是让聚合根只暴露命令方法(如 Ship()),内部校验后生成事件、应用到自身,并返回事件列表供外部持久化:
public class Order
{
private readonly List<IEvent> _uncommittedEvents = new();
public Guid Id { get; private set; }
public int Version { get; private set; }
public OrderStatus Status { get; private set; }
<pre class="brush:php;toolbar:false;">// 从快照或事件流重建时使用
private Order() { }
public static Order Create(Guid id) => new() { Id = id, Status = OrderStatus.Created };
public void Ship()
{
if (Status != OrderStatus.Confirmed)
throw new InvalidOperationException("Only confirmed orders can be shipped");
var @event = new OrderShippedEvent(Id, DateTime.UtcNow);
Apply(@event); // 内部更新状态
_uncommittedEvents.Add(@event);
}
private void Apply(OrderShippedEvent e)
{
Status = OrderStatus.Shipped;
Version++;
}
public IReadOnlyList<IEvent> DequeueUncommittedEvents()
=> _uncommittedEvents.ToList().AsReadOnly();}
- 聚合根不暴露 setter,所有状态变更必须经由明确命令方法
-
Apply()方法必须同步更新内存状态,并严格对应事件语义(比如OrderShippedEvent→Status = Shipped) - 不要在
Apply()里做 I/O 或跨聚合调用——那是应用服务层的事 -
DequeueUncommittedEvents()是关键桥梁:它把内存事件吐给仓储层落库,之后清空
并发冲突时 OptimisticConcurrencyException 怎么处理?
事件溯源天然依赖乐观并发控制。当两个线程同时加载版本 5 的订单、都生成了 OrderShippedEvent,第二个写入会因 expectedVersion=5 不匹配而失败,抛出 OptimisticConcurrencyException。这不是 bug,是设计预期。
- 绝对不要静默捕获并重试——业务语义可能已失效(比如库存已被扣完)
- 应用服务层应捕获该异常,根据上下文决定:
放弃操作、合并事件(如转为OrderPartiallyShippedEvent)、或转入人工审核队列 - SQL Server 用户注意:
rowversion列是默认选择;Cosmos DB 用户需用_etag字段做条件更新 - 测试时务必模拟并发:用
Parallel.For同时调用同一聚合的命令,验证异常是否被正确抛出和处理
事件溯源真正的复杂点不在代码怎么写,而在于事件粒度是否真实反映业务意图、快照策略是否平衡重建性能与存储成本、以及读模型(CQRS 中的 Query Side)能否独立演进而不受写模型拖累。别急着堆砌框架,先用纸笔画出你的核心聚合生命周期——从创建到关闭,每一步“发生了什么事实”,再决定哪些该成为事件。


















