如何在 C# 中處理聚合根之間的關(guān)系?

你想知道在 C# 中遵循 DDD 原則處理多個(gè)聚合根之間關(guān)聯(lián)關(guān)系的方法,核心是既要保證每個(gè)聚合根的獨(dú)立性和邊界完整性,又能實(shí)現(xiàn)跨聚合的業(yè)務(wù)交互,同時(shí)避免強(qiáng)耦合、數(shù)據(jù)不一致或循環(huán)依賴等問題。

聚合根之間的關(guān)系處理是 DDD 落地的核心難點(diǎn)之一,C# 中有一套貼合語言特性(如強(qiáng)類型、依賴注入、事件機(jī)制)的最佳實(shí)踐,下面從核心原則、具體實(shí)現(xiàn)方式、避坑指南三個(gè)維度展開講解。

一、處理聚合根關(guān)系的核心原則(C# 適配版)

在動(dòng)手編碼前,先明確 3 個(gè)不可違背的原則,這是保證聚合邊界不被破壞的基礎(chǔ):

  1. 僅通過 ID 關(guān)聯(lián),不直接持有對(duì)象引用
    聚合根內(nèi)只存儲(chǔ)其他聚合根的強(qiáng)類型 ID(而非裸 Guid/int,更非對(duì)象實(shí)例),避免跨聚合的強(qiáng)耦合,也解決持久化時(shí)的級(jí)聯(lián)加載/循環(huán)引用問題。
  2. 單向關(guān)聯(lián)優(yōu)先,避免雙向依賴
    盡量設(shè)計(jì)「單向 ID 引用」(如 Order → UserId),而非雙向關(guān)聯(lián)(Order→UserId + User→List<OrderId>),除非有強(qiáng)業(yè)務(wù)需求(如“用戶必須查看所有訂單”),否則會(huì)大幅增加復(fù)雜度。
  3. 跨聚合邏輯通過領(lǐng)域服務(wù)/領(lǐng)域事件協(xié)調(diào)
    聚合根內(nèi)部不直接調(diào)用其他聚合根的方法,跨聚合的業(yè)務(wù)規(guī)則交給領(lǐng)域服務(wù),跨聚合的狀態(tài)同步交給領(lǐng)域事件,保障聚合根的內(nèi)聚性。

二、C# 中聚合根關(guān)系的具體實(shí)現(xiàn)方式

1. 基礎(chǔ)實(shí)現(xiàn):用強(qiáng)類型 ID 存儲(chǔ)關(guān)聯(lián)(核心)

C# 中推薦用 record 定義強(qiáng)類型 ID(替代裸 Guid/int),避免不同聚合根的 ID 混用(比如把 OrderId 傳給 UserId 參數(shù))。

// 1. 定義各聚合根的強(qiáng)類型ID(C# record 天然支持值相等、不可變) public record UserId(Guid Value); public record OrderId(Guid Value); public record ProductId(Guid Value); // 2. 聚合根1:User(用戶)—— 獨(dú)立聚合根 public class User { public UserId Id { get; } public string Name { get; } public bool IsFrozen { get; private set; } // 用戶狀態(tài):是否凍結(jié) public int PaidOrderCount { get; private set; } // 已支付訂單數(shù) 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; } // 工廠方法創(chuàng)建 public static User Create(string name) => new User(new UserId(Guid.NewGuid()), name); // 聚合根內(nèi)部行為:更新已支付訂單數(shù)(僅內(nèi)部/領(lǐng)域事件可調(diào)用) internal void IncrementPaidOrderCount() => PaidOrderCount++; } // 3. 聚合根2:Order(訂單)—— 關(guān)聯(lián)User的ID(僅存ID,不存User對(duì)象) public class Order { // 核心屬性:僅存儲(chǔ)關(guān)聯(lián)聚合根的ID public OrderId Id { get; } public UserId UserId { get; } // 關(guān)聯(lián)用戶ID(核心:僅存ID,非對(duì)象) public OrderStatus Status { get; private set; } // 領(lǐng)域事件容器(用于跨聚合通信) 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; } // 工廠方法創(chuàng)建訂單(僅依賴UserId) public static Order Create(UserId userId) => new Order(new OrderId(Guid.NewGuid()), userId); // 聚合根行為:標(biāo)記訂單為已支付(觸發(fā)領(lǐng)域事件) public void MarkAsPaid() { if (Status != OrderStatus.PendingPayment) throw new InvalidOperationException("僅待支付訂單可支付"); Status = OrderStatus.Paid; // 發(fā)布領(lǐng)域事件:訂單支付成功(用于跨聚合同步) _domainEvents.Add(new OrderPaidDomainEvent(Id, UserId)); } // 清空領(lǐng)域事件(倉儲(chǔ)保存后調(diào)用) public void ClearDomainEvents() => _domainEvents.Clear(); } public enum OrderStatus { PendingPayment, // 待支付 Paid, // 已支付 Cancelled // 已取消 }

2. 按需加載關(guān)聯(lián)的聚合根(通過倉儲(chǔ))

當(dāng)業(yè)務(wù)需要訪問關(guān)聯(lián)聚合根的信息時(shí),通過倉儲(chǔ)根據(jù) ID 查詢,而非在聚合根內(nèi)直接持有對(duì)象引用(避免聚合邊界被突破)。

// 1. 倉儲(chǔ)接口(僅針對(duì)聚合根設(shè)計(jì)) public interface IUserRepository { Task<User?> FindByIdAsync(UserId userId, CancellationToken ct = default); Task SaveAsync(User user, CancellationToken ct = default); } public interface IOrderRepository { Task<List<Order>> FindByUserIdAsync(UserId userId, CancellationToken ct = default); Task SaveAsync(Order order, CancellationToken ct = default); } // 2. 領(lǐng)域服務(wù):按需加載關(guān)聯(lián)聚合根(查詢用戶+用戶的所有訂單) public class OrderQueryService { private readonly IOrderRepository _orderRepo; private readonly IUserRepository _userRepo; public OrderQueryService(IOrderRepository orderRepo, IUserRepository userRepo) { _orderRepo = orderRepo; _userRepo = userRepo; } // 核心邏輯:先查用戶,再根據(jù)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. 查詢?cè)撚脩舻乃杏唵尉酆细?/span> var orders = await _orderRepo.FindByUserIdAsync(userId); return (user, orders); } }

3. 跨聚合業(yè)務(wù)規(guī)則:用領(lǐng)域服務(wù)協(xié)調(diào)

當(dāng)需要跨聚合根校驗(yàn)業(yè)務(wù)規(guī)則(如“凍結(jié)用戶不能創(chuàng)建訂單”)時(shí),不要在一個(gè)聚合根內(nèi)依賴另一個(gè)聚合根,而是通過領(lǐng)域服務(wù)整合。

// 領(lǐng)域服務(wù):處理跨聚合的訂單創(chuàng)建邏輯 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. 跨聚合校驗(yàn):查詢用戶并校驗(yàn)狀態(tài)(聚合根不直接依賴,由領(lǐng)域服務(wù)協(xié)調(diào)) var user = await _userRepo.FindByIdAsync(userId) ?? throw new InvalidOperationException("用戶不存在"); if (user.IsFrozen) throw new InvalidOperationException("用戶已被凍結(jié),無法創(chuàng)建訂單"); // 2. 創(chuàng)建訂單聚合根(僅依賴UserId,不依賴User對(duì)象) var order = Order.Create(userId); // 3. 可選:校驗(yàn)商品(另一個(gè)聚合根)的庫存 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; } }

4. 跨聚合數(shù)據(jù)一致性:用領(lǐng)域事件(C# 實(shí)現(xiàn))

當(dāng)一個(gè)聚合根的狀態(tài)變更需要同步到另一個(gè)聚合根(如“訂單支付成功后,更新用戶的已支付訂單數(shù)”),通過領(lǐng)域事件異步處理,避免分布式事務(wù)。C# 中常用 MediatR 實(shí)現(xiàn)事件發(fā)布/訂閱。

// 第一步:定義領(lǐng)域事件(C# record 天然適合做事件) public record OrderPaidDomainEvent(OrderId OrderId, UserId UserId) : INotification; // 第二步:實(shí)現(xiàn)事件處理器(處理跨聚合邏輯) public class OrderPaidDomainEventHandler : INotificationHandler<OrderPaidDomainEvent> { private readonly IUserRepository _userRepo; public OrderPaidDomainEventHandler(IUserRepository userRepo) { _userRepo = userRepo; } // 事件處理邏輯:更新用戶的已支付訂單數(shù) public async Task Handle(OrderPaidDomainEvent notification, CancellationToken cancellationToken) { var user = await _userRepo.FindByIdAsync(notification.UserId, cancellationToken); if (user != null) { user.IncrementPaidOrderCount(); // 調(diào)用User聚合根的內(nèi)部方法 await _userRepo.SaveAsync(user, cancellationToken); } } } // 第三步:倉儲(chǔ)保存時(shí)觸發(fā)事件(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. 發(fā)布所有領(lǐng)域事件(觸發(fā)跨聚合邏輯) foreach (var domainEvent in order.DomainEvents) { await _mediator.Publish(domainEvent, ct); } // 3. 清空事件,避免重復(fù)發(fā)布 order.ClearDomainEvents(); } }

三、C# 中處理聚合根關(guān)系的避坑指南

常見錯(cuò)誤 問題后果 C# 正確做法
聚合根內(nèi)直接持有其他聚合根對(duì)象 強(qiáng)耦合、循環(huán)引用、持久化級(jí)聯(lián)加載異常 僅存儲(chǔ)強(qiáng)類型 ID,按需通過倉儲(chǔ)查詢
使用裸 Guid/int 作為關(guān)聯(lián) ID 容易傳錯(cuò)參數(shù)(如把 OrderId 傳給 UserId) record 定義強(qiáng)類型 ID(如 UserId/OrderId
在聚合根構(gòu)造函數(shù)/方法中依賴其他聚合根 聚合根內(nèi)聚性被破壞,測(cè)試難度增加 跨聚合邏輯交給領(lǐng)域服務(wù),聚合根僅依賴自身狀態(tài)
雙向關(guān)聯(lián)(User 存 OrderId 列表 + Order 存 UserId) 復(fù)雜度飆升,易出現(xiàn)數(shù)據(jù)不一致 優(yōu)先單向關(guān)聯(lián),僅在業(yè)務(wù)必需時(shí)保留雙向 ID 引用(不存對(duì)象)

總結(jié)

處理 C# 中聚合根之間的關(guān)系,核心要抓住 3 個(gè)關(guān)鍵點(diǎn):

  1. 關(guān)聯(lián)方式:僅通過強(qiáng)類型 ID 關(guān)聯(lián)(用 record 實(shí)現(xiàn)),不直接持有其他聚合根對(duì)象,避免耦合;
  2. 規(guī)則協(xié)調(diào):跨聚合的業(yè)務(wù)規(guī)則交給領(lǐng)域服務(wù)處理,聚合根內(nèi)只封裝自身的核心邏輯;
  3. 狀態(tài)同步:跨聚合的狀態(tài)變更通過領(lǐng)域事件(如 MediatR) 異步處理,保障數(shù)據(jù)一致性且避免分布式事務(wù)。

這套方式既符合 DDD 的聚合邊界原則,又充分利用了 C# 的語言特性(強(qiáng)類型、record、依賴注入),是工業(yè)級(jí)項(xiàng)目中最常用的實(shí)踐方案。

本文使用 文章同步助手 同步

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容