商城首页欢迎来到中国正版软件门户

您的位置:首页 >c#如何使用事件溯源_c#事件溯源完整教程与代码实例

c#如何使用事件溯源_c#事件溯源完整教程与代码实例

  发布于2026-05-03 阅读(0)

扫一扫,手机访问

C#事件溯源:从“加个event”到状态管理哲学的跨越

c#如何使用事件溯源_c#事件溯源完整教程与代码实例

事件溯源(Event Sourcing)远不止是“给类加个 event 关键字”那么简单。它本质上是一套状态管理哲学,其核心在于用不可变的事件序列来替代直接的状态更新。如果你正在考虑在C#项目中引入事件溯源,首先要问自己两个问题:你的业务模型是否真的需要审计追踪、状态重放和最终一致性?以及,你是否准备好应对由此带来的聚合重建、快照管理和并发冲突等额外的复杂度?

为什么不能直接用 public event EventHandler

这是最常见的误区:将.NET语言层面的event关键字与领域驱动设计中的“事件溯源”概念混为一谈。前者只是一种基于委托的发布-订阅机制,常用于UI响应或模块解耦;后者则是一套要求将业务事实作为持久化、有序且带版本信息的事件日志的完整模式。用event触发一次OrderShippedEvent,并不意味着这个事件被写入了EventStore,更不意味着能依靠它来重建聚合的完整状态。

  • 语言级事件(event):是内存中的一次性通知,缺乏持久化保证、顺序性和版本控制。
  • 事件溯源要求:每个领域事件都必须满足:追加写入存储携带聚合根ID和版本号能够被重放以重建任意时间点的状态
  • 典型错误:在OnOrderShipped()方法中仅仅触发了OrderShipped事件,却忘记了调用_eventStore.AppendAsync(aggregateId, new OrderShippedEvent(...), expectedVersion)来完成事件的持久化。

如何正确建模一个可溯源的订单聚合?

关键在于如何设计状态变更的入口,而非仅仅编写事件类。在C#中,标准的做法是让聚合根只暴露命令方法(例如Ship()),在方法内部进行业务校验,生成对应的事件,将事件应用到聚合自身以更新状态,并最终返回事件列表供外部基础设施持久化。

public class Order
{
    private readonly List _uncommittedEvents = new();
    public Guid Id { get; private set; }
    public int Version { get; private set; }
    public OrderStatus Status { get; private set; }

    // 从快照或事件流重建时使用的私有构造函数
    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 DequeueUncommittedEvents()
        => _uncommittedEvents.ToList().AsReadOnly();
}
  • 状态封装:聚合根不暴露公共的setter,所有状态变更必须通过定义明确的命令方法来驱动。
  • 事件应用Apply()方法必须严格地根据事件的语义同步更新聚合的内存状态(例如,OrderShippedEvent对应将Status设置为Shipped)。
  • 职责分离:切忌在Apply()方法中执行I/O操作或进行跨聚合调用,这些属于应用服务层的职责。
  • 事件提交DequeueUncommittedEvents()方法是连接领域层与基础设施层的关键桥梁,它负责将聚合在本次操作中产生的新事件取出,交由仓储层持久化到事件存储中,之后清空未提交事件列表。

并发冲突时 OptimisticConcurrencyException 怎么处理?

事件溯源模式天然依赖于乐观并发控制。设想这样一个场景:两个线程同时加载了版本号为5的同一个订单聚合,并都试图执行发货操作生成OrderShippedEvent。当第二个线程尝试以expectedVersion=5的条件追加事件时,会发现实际版本号已不匹配,从而抛出OptimisticConcurrencyException。这并非系统缺陷,而是设计上的预期行为。

  • 禁止静默处理:绝对不要简单地捕获此异常并静默重试,因为此时业务上下文可能已失效(例如库存已被另一操作扣完)。
  • 策略性处理:应用服务层应捕获该异常,并根据具体业务场景决定后续策略:是放弃当前操作、尝试合并事件(例如,转换为OrderPartiallyShippedEvent),还是将操作转入人工审核队列
  • 存储适配:使用SQL Server时,rowversion列是实现乐观并发的常规选择;若使用Cosmos DB,则需要依赖其_etag字段进行条件更新。
  • 测试验证:务必在测试阶段模拟并发场景,例如使用Parallel.For同时调用对同一聚合的命令,以验证并发异常是否能被正确抛出和处理。

事件溯源真正的挑战,往往不在于具体的代码如何编写,而在于前期的设计决策:事件粒度的划分是否精准反映了业务意图?快照策略能否在状态重建性能和存储成本之间取得平衡?读模型(在CQRS架构中)能否独立于写模型进行演进?在引入任何框架之前,不妨先用纸笔勾勒出核心聚合的完整生命周期——从创建到终结,每一步“发生了什么事实”,然后再决定哪些事实值得被记录为事件。这才是驾驭事件溯源的起点。

本文转载于:https://www.php.cn/faq/2325026.html 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。

热门关注