你想知道在 C# 中遵循 DDD 原则处理多个聚合根之间关联关系的方法,核心是既要保证每个聚合根的独立性和边界完整性,又能实现跨聚合的业务交互,同时避免强耦合、数据不一致或循环依赖等问题。
聚合根之间的关系处理是 DDD 落地的核心难点之一,C# 中有一套贴合语言特性(如强类型、依赖注入、事件机制)的最佳实践,下面从核心原则、具体实现方式、避坑指南三个维度展开讲解。
一、处理聚合根关系的核心原则(C# 适配版)
在动手编码前,先明确 3 个不可违背的原则,这是保证聚合边界不被破坏的基础:
仅通过 ID 关联,不直接持有对象引用
聚合根内只存储其他聚合根的强类型 ID(而非裸 Guid/int,更非对象实例),避免跨聚合的强耦合,也解决持久化时的级联加载/循环引用问题。
单向关联优先,避免双向依赖
尽量设计「单向 ID 引用」(如 Order → UserId),而非双向关联(Order→UserId + User→List),除非有强业务需求(如“用户必须查看所有订单”),否则会大幅增加复杂度。
跨聚合逻辑通过领域服务/领域事件协调
聚合根内部不直接调用其他聚合根的方法,跨聚合的业务规则交给领域服务,跨聚合的状态同步交给领域事件,保障聚合根的内聚性。
二、C# 中聚合根关系的具体实现方式
- 基础实现:用强类型 ID 存储关联(核心)
C# 中推荐用 record 定义强类型 ID(替代裸 Guid/int),避免不同聚合根的 ID 混用(比如把 OrderId 传给 UserId 参数)。
// 1. 定义各聚合根的强类型ID(C# record 天然支持值相等、不可变)
public record UserId(Guid Value);
public record OrderId(Guid Value);
public record ProductId(Guid Value);
// 2. 聚合根1:User(用户)—— 独立聚合根
public class User
{
public UserId Id { get; }
public string Name { get; }
public bool IsFrozen { get; private set; } // 用户状态:是否冻结
public int PaidOrderCount { get; private set; } // 已支付订单数
private User(UserId id, string name)
{
Id = id ?? throw new ArgumentNullException(nameof(id));
Name = !string.IsNullOrWhiteSpace(name) ? name : throw new ArgumentException("用户名不能为空");
IsFrozen = false;
PaidOrderCount = 0;
}
// 工厂方法创建
public static User Create(string name) => new User(new UserId(Guid.NewGuid()), name);
// 聚合根内部行为:更新已支付订单数(仅内部/领域事件可调用)
internal void IncrementPaidOrderCount() => PaidOrderCount++;
}
// 3. 聚合根2:Order(订单)—— 关联User的ID(仅存ID,不存User对象)
public class Order
{
// 核心属性:仅存储关联聚合根的ID
public OrderId Id { get; }
public UserId UserId { get; } // 关联用户ID(核心:仅存ID,非对象)
public OrderStatus Status { get; private set; }
// 领域事件容器(用于跨聚合通信)
private readonly List<INotification> _domainEvents = new();
public IReadOnlyCollection<INotification> DomainEvents => _domainEvents.AsReadOnly();
private Order(OrderId id, UserId userId)
{
Id = id ?? throw new ArgumentNullException(nameof(id));
UserId = userId ?? throw new ArgumentNullException(nameof(userId));
Status = OrderStatus.PendingPayment;
}
// 工厂方法创建订单(仅依赖UserId)
public static Order Create(UserId userId) => new Order(new OrderId(Guid.NewGuid()), userId);
// 聚合根行为:标记订单为已支付(触发领域事件)
public void MarkAsPaid()
{
if (Status != OrderStatus.PendingPayment)
throw new InvalidOperationException("仅待支付订单可支付");
Status = OrderStatus.Paid;
// 发布领域事件:订单支付成功(用于跨聚合同步)
_domainEvents.Add(new OrderPaidDomainEvent(Id, UserId));
}
// 清空领域事件(仓储保存后调用)
public void ClearDomainEvents() => _domainEvents.Clear();
}
public enum OrderStatus
{
PendingPayment, // 待支付
Paid, // 已支付
Cancelled // 已取消
}
- 按需加载关联的聚合根(通过仓储)
当业务需要访问关联聚合根的信息时,通过仓储根据 ID 查询,而非在聚合根内直接持有对象引用(避免聚合边界被突破)。
// 1. 仓储接口(仅针对聚合根设计)
public interface IUserRepository
{
Task FindByIdAsync(UserId userId, CancellationToken ct = default);
Task SaveAsync(User user, CancellationToken ct = default);
}
public interface IOrderRepository
{
Task> FindByUserIdAsync(UserId userId, CancellationToken ct = default);
Task SaveAsync(Order order, CancellationToken ct = default);
}
// 2. 领域服务:按需加载关联聚合根(查询用户+用户的所有订单)
public class OrderQueryService
{
private readonly IOrderRepository _orderRepo;
private readonly IUserRepository _userRepo;
public OrderQueryService(IOrderRepository orderRepo, IUserRepository userRepo)
{
_orderRepo = orderRepo;
_userRepo = userRepo;
}
// 核心逻辑:先查用户,再根据UserId查订单(按需加载,不耦合)
public async Task<(User User, List<Order> Orders)> GetUserOrdersAsync(UserId userId)
{
// 1. 查询用户聚合根
var user = await _userRepo.FindByIdAsync(userId)
?? throw new KeyNotFoundException($"用户ID {userId.Value} 不存在");
// 2. 查询该用户的所有订单聚合根
var orders = await _orderRepo.FindByUserIdAsync(userId);
return (user, orders);
}
}
- 跨聚合业务规则:用领域服务协调
当需要跨聚合根校验业务规则(如“冻结用户不能创建订单”)时,不要在一个聚合根内依赖另一个聚合根,而是通过领域服务整合。
// 领域服务:处理跨聚合的订单创建逻辑
public class OrderCreationService
{
private readonly IOrderRepository _orderRepo;
private readonly IUserRepository _userRepo;
private readonly IStockService _stockService;
public OrderCreationService(IOrderRepository orderRepo, IUserRepository userRepo, IStockService stockService)
{
_orderRepo = orderRepo;
_userRepo = userRepo;
_stockService = stockService;
}
public async Task<Order> CreateOrderAsync(UserId userId, List<OrderItemDto> itemDtos)
{
// 1. 跨聚合校验:查询用户并校验状态(聚合根不直接依赖,由领域服务协调)
var user = await _userRepo.FindByIdAsync(userId)
?? throw new InvalidOperationException("用户不存在");
if (user.IsFrozen)
throw new InvalidOperationException("用户已被冻结,无法创建订单");
// 2. 创建订单聚合根(仅依赖UserId,不依赖User对象)
var order = Order.Create(userId);
// 3. 可选:校验商品(另一个聚合根)的库存
foreach (var itemDto in itemDtos)
{
var productId = new ProductId(itemDto.ProductId);
var product = await _productRepo.FindByIdAsync(productId);
order.AddItem(product, itemDto.Quantity, _stockService);
}
// 4. 保存订单聚合根
await _orderRepo.SaveAsync(order);
return order;
}
}
- 跨聚合数据一致性:用领域事件(C# 实现)
当一个聚合根的状态变更需要同步到另一个聚合根(如“订单支付成功后,更新用户的已支付订单数”),通过领域事件异步处理,避免分布式事务。C# 中常用 MediatR 实现事件发布/订阅。
// 第一步:定义领域事件(C# record 天然适合做事件)
public record OrderPaidDomainEvent(OrderId OrderId, UserId UserId) : INotification;
// 第二步:实现事件处理器(处理跨聚合逻辑)
public class OrderPaidDomainEventHandler : INotificationHandler
{
private readonly IUserRepository _userRepo;
public OrderPaidDomainEventHandler(IUserRepository userRepo)
{
_userRepo = userRepo;
}
// 事件处理逻辑:更新用户的已支付订单数
public async Task Handle(OrderPaidDomainEvent notification, CancellationToken cancellationToken)
{
var user = await _userRepo.FindByIdAsync(notification.UserId, cancellationToken);
if (user != null)
{
user.IncrementPaidOrderCount(); // 调用User聚合根的内部方法
await _userRepo.SaveAsync(user, cancellationToken);
}
}
}
// 第三步:仓储保存时触发事件(EF Core 示例)
public class EfCoreOrderRepository : IOrderRepository
{
private readonly AppDbContext _dbContext;
private readonly IMediator _mediator;
public EfCoreOrderRepository(AppDbContext dbContext, IMediator mediator)
{
_dbContext = dbContext;
_mediator = mediator;
}
public async Task SaveAsync(Order order, CancellationToken ct = default)
{
// 1. 保存订单聚合根
if (_dbContext.Orders.Contains(order))
_dbContext.Update(order);
else
_dbContext.Add(order);
await _dbContext.SaveChangesAsync(ct);
// 2. 发布所有领域事件(触发跨聚合逻辑)
foreach (var domainEvent in order.DomainEvents)
{
await _mediator.Publish(domainEvent, ct);
}
// 3. 清空事件,避免重复发布
order.ClearDomainEvents();
}
}
三、C# 中处理聚合根关系的避坑指南
常见错误 问题后果 C# 正确做法
聚合根内直接持有其他聚合根对象 强耦合、循环引用、持久化级联加载异常 仅存储强类型 ID,按需通过仓储查询
使用裸 Guid/int 作为关联 ID 容易传错参数(如把 OrderId 传给 UserId) 用 record 定义强类型 ID(如 UserId/OrderId)
在聚合根构造函数/方法中依赖其他聚合根 聚合根内聚性被破坏,测试难度增加 跨聚合逻辑交给领域服务,聚合根仅依赖自身状态
双向关联(User 存 OrderId 列表 + Order 存 UserId) 复杂度飙升,易出现数据不一致 优先单向关联,仅在业务必需时保留双向 ID 引用(不存对象)
总结
处理 C# 中聚合根之间的关系,核心要抓住 3 个关键点:
关联方式:仅通过强类型 ID 关联(用 record 实现),不直接持有其他聚合根对象,避免耦合;
规则协调:跨聚合的业务规则交给领域服务处理,聚合根内只封装自身的核心逻辑;
状态同步:跨聚合的状态变更通过领域事件(如 MediatR) 异步处理,保障数据一致性且避免分布式事务。
这套方式既符合 DDD 的聚合边界原则,又充分利用了 C# 的语言特性(强类型、record、依赖注入),是工业级项目中最常用的实践方案。