C# DDD聚合根实现方法 C#如何设计和实现Aggregate Root

来源:这里教程网 时间:2026-02-21 17:40:45 作者:

聚合根必须显式控制内部实体的生命周期

DDD 中的聚合根不是靠注释或文档约定出来的,而是通过代码强制约束:外部只能持有聚合根的引用,不能直接 new 或访问内部实体。常见错误是让仓储返回

OrderItem
实体,或者在应用层调用
order.Items.Add(...)
后再保存——这会绕过聚合根的不变量校验。

正确做法是只暴露方法而非集合属性:

public class Order : AggregateRoot
{
    private readonly List<OrderItem> _items = new();
    
    // ❌ 错误:返回可修改集合引用
    // public IReadOnlyList<OrderItem> Items => _items.AsReadOnly();
    
    // ✅ 正确:通过行为方法封装变更
    public void AddItem(ProductId productId, int quantity)
    {
        if (quantity <= 0) throw new ArgumentException("Quantity must be positive");
        if (_items.Count >= 100) throw new DomainException("Max 100 items per order");
        
        _items.Add(new OrderItem(productId, quantity));
        AddDomainEvent(new OrderItemAddedDomainEvent(Id, productId, quantity));
    }
}
所有对内部状态的修改必须走聚合根定义的方法,确保每次变更都经过业务规则检查 不要暴露
_items
的 setter 或可写集合接口(如
IList<t></t>
若需查询内部数据,返回不可变副本(
IReadOnlyList<t></t>
)或 DTO,而非原始引用

聚合根 ID 必须在构造时生成且不可变

聚合根的 ID 是其身份标识,也是仓储定位聚合的唯一依据。如果允许运行时修改

Id
,会导致仓储找不到聚合、事件溯源错乱、并发冲突无法识别等问题。

典型错误包括:用默认构造函数 + 属性赋值、从数据库读取后重设 ID、使用 ORM 的延迟加载代理覆盖 ID 字段。

✅ 构造函数中生成 ID:
public Order(OrderId id) : base(id ?? OrderId.New()) { }
✅ 使用只读属性:
public OrderId Id { get; } // 不是 get; set;
❌ 避免 EF Core 的
ValueConverter
或自定义 setter 干预 ID 赋值逻辑
⚠️ 若用 EF Core,需配置
HasIndex(e => e.Id).IsUnique()
并禁用 ID 的 update 操作

仓储接口只能操作聚合根,不能暴露内部实体的 CRUD

仓储(Repository)是聚合根的「专属门面」,它的泛型参数必须是聚合根类型,比如

IRepository<order></order>
。一旦出现
IRepository<orderitem></orderitem>
,说明聚合边界设计失败,或者把技术存储细节泄露到了领域层。

错误信号包括:

应用服务里调用
_orderItemRepo.FindById(...)
仓储方法返回
IQueryable<orderitem></orderitem>
或接受
OrderItem
作为参数
在仓储实现中手动组装
Order
和它的
OrderItem
列表(应由聚合根自己重建)

正确做法是让仓储只负责整个聚合的加载与保存:

public interface IRepository<TAggregate> where TAggregate : AggregateRoot
{
    Task<TAggregate> GetByIdAsync(AggregateId id, CancellationToken ct = default);
    Task SaveAsync(TAggregate aggregate, CancellationToken ct = default);
}
// 应用服务中:
var order = await _orderRepo.GetByIdAsync(orderId);
order.AddItem(productId, 2); // 在内存中变更
await _orderRepo.SaveAsync(order); // 一次性持久化整个聚合

事件溯源场景下,聚合根需基于事件流重建状态

如果采用事件溯源(Event Sourcing),聚合根不能依赖数据库快照,而必须能从一组事件中完整重建自身。这意味着构造函数要支持「空 ID + 重放事件」模式,且所有状态变更必须由事件驱动。

聚合根需提供静态工厂方法:
public static Order Rehydrate(IEnumerable<idomainevent> events)</idomainevent>
每个事件处理方法(如
When(OrderPlaced e)
)只做状态变更,不触发新业务逻辑
避免在
When
方法中调用外部服务、发邮件、查数据库——这些属于应用层职责
EF Core 等 ORM 通常不适用于事件溯源;推荐用专用事件存储(如 EventStoreDB)或自建 append-only 表

最易被忽略的是:事件类本身必须是不可变的纯数据容器,且版本号、时间戳、聚合 ID 等元信息应在基础设施层注入,而非由聚合根构造。

相关推荐