如何在 C# 中处理聚合根之间的关系?

简介: 本文详解C#中遵循DDD原则处理聚合根关联的最佳实践:通过强类型ID关联、领域服务协调业务规则、领域事件实现跨聚合状态同步,结合MediatR与仓储模式,保障聚合边界完整性,避免循环依赖与数据不一致,提升系统可维护性与扩展性。

你想知道在 C# 中遵循 DDD 原则处理多个聚合根之间关联关系的方法,核心是既要保证每个聚合根的独立性和边界完整性,又能实现跨聚合的业务交互,同时避免强耦合、数据不一致或循环依赖等问题。

聚合根之间的关系处理是 DDD 落地的核心难点之一,C# 中有一套贴合语言特性(如强类型、依赖注入、事件机制)的最佳实践,下面从核心原则、具体实现方式、避坑指南三个维度展开讲解。

一、处理聚合根关系的核心原则(C# 适配版)
在动手编码前,先明确 3 个不可违背的原则,这是保证聚合边界不被破坏的基础:

仅通过 ID 关联,不直接持有对象引用
聚合根内只存储其他聚合根的强类型 ID(而非裸 Guid/int,更非对象实例),避免跨聚合的强耦合,也解决持久化时的级联加载/循环引用问题。
单向关联优先,避免双向依赖
尽量设计「单向 ID 引用」(如 Order → UserId),而非双向关联(Order→UserId + User→List),除非有强业务需求(如“用户必须查看所有订单”),否则会大幅增加复杂度。
跨聚合逻辑通过领域服务/领域事件协调
聚合根内部不直接调用其他聚合根的方法,跨聚合的业务规则交给领域服务,跨聚合的状态同步交给领域事件,保障聚合根的内聚性。
二、C# 中聚合根关系的具体实现方式

  1. 基础实现:用强类型 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 // 已取消
}

  1. 按需加载关联的聚合根(通过仓储)
    当业务需要访问关联聚合根的信息时,通过仓储根据 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);
}

}

  1. 跨聚合业务规则:用领域服务协调
    当需要跨聚合根校验业务规则(如“冻结用户不能创建订单”)时,不要在一个聚合根内依赖另一个聚合根,而是通过领域服务整合。

// 领域服务:处理跨聚合的订单创建逻辑
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;
}

}

  1. 跨聚合数据一致性:用领域事件(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、依赖注入),是工业级项目中最常用的实践方案。

相关文章
C# DEV 关于设置gridview 指定单元格字体为红色
C# DEV 关于设置gridview 指定单元格字体为红色
|
存储 云安全 运维
阿里云认证介绍 - 线上考试报名指南(ACA/ACP/ACE)
阿里云认证介绍 - 线上考试报名指南(ACA/ACP/ACE)
|
C# Windows 容器
C#或Winform中的消息通知之系统托盘的气泡提示窗口(系统toast通知)、ToolTip控件和ToolTipText属性
NotifyIcon控件表示系统右下角任务栏上的托盘图标,其ShowBalloonTip方法用于显示气球状提示框(Win10只有为本地Toast通知),ToolTip\oolTipText可以...
3396 0
C#或Winform中的消息通知之系统托盘的气泡提示窗口(系统toast通知)、ToolTip控件和ToolTipText属性
|
3天前
|
人工智能 边缘计算 C#
纯C#驱动全场景视觉AI:.NET 10+YOLO多模型平台赋能工业与边缘智能
基于.NET 10与YOLO技术的纯C#视觉AI平台,支持多模型并行、跨平台部署,集成目标检测、分割、姿态估计等全任务,无需Python依赖,助力工业质检、智能安防、零售分析等场景高效落地。
|
SQL C# 数据库
EPPlus库的安装和使用 C# 中 Excel的导入和导出
本文介绍了如何使用EPPlus库在C#中实现Excel的导入和导出功能。首先,通过NuGet包管理器安装EPPlus库,然后提供了将DataGridView数据导出到Excel的步骤和代码示例,包括将DataGridView转换为DataTable和使用EPPlus将DataTable导出为Excel文件。接着,介绍了如何将Excel数据导入到数据库中,包括读取Excel文件、解析数据、执行SQL插入操作。
EPPlus库的安装和使用 C# 中 Excel的导入和导出
|
设计模式 开发框架 前端开发
在DevExpress中使用BandedGridView表格实现多行表头的处理
在DevExpress中使用BandedGridView表格实现多行表头的处理
|
开发框架 缓存 前端开发
使用扩展函数方式,在Winform界面中快捷的绑定树形列表TreeList控件和TreeListLookUpEdit控件
使用扩展函数方式,在Winform界面中快捷的绑定树形列表TreeList控件和TreeListLookUpEdit控件
|
开发框架 前端开发 JavaScript
在Winform界面使用自定义用户控件及TabelPanel和StackPanel布局控件
在Winform界面使用自定义用户控件及TabelPanel和StackPanel布局控件
|
开发工具
新人乘风者礼品兑换指南
仅限2023年11月15日(含11月15日)后入驻博主用于兑换礼品,此前完成入驻的博主按原邮寄方式进行。
4726 9
|
存储 Java 测试技术
阿里巴巴java开发手册
这篇文章是关于阿里巴巴Java开发手册的整理,内容包括编程规约、异常日志、单元测试、安全规约、MySQL数据库使用以及工程结构等方面的详细规范和建议,旨在帮助开发者编写更加规范、高效和安全的代码。

热门文章

最新文章