你想知道在 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ǔ):
-
僅通過 ID 關(guān)聯(lián),不直接持有對(duì)象引用
聚合根內(nèi)只存儲(chǔ)其他聚合根的強(qiáng)類型 ID(而非裸Guid/int,更非對(duì)象實(shí)例),避免跨聚合的強(qiáng)耦合,也解決持久化時(shí)的級(jí)聯(lián)加載/循環(huán)引用問題。 -
單向關(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ù)雜度。 -
跨聚合邏輯通過領(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):
-
關(guān)聯(lián)方式:僅通過強(qiáng)類型 ID 關(guān)聯(lián)(用
record實(shí)現(xiàn)),不直接持有其他聚合根對(duì)象,避免耦合; - 規(guī)則協(xié)調(diào):跨聚合的業(yè)務(wù)規(guī)則交給領(lǐng)域服務(wù)處理,聚合根內(nèi)只封裝自身的核心邏輯;
- 狀態(tài)同步:跨聚合的狀態(tài)變更通過領(lǐng)域事件(如 MediatR) 異步處理,保障數(shù)據(jù)一致性且避免分布式事務(wù)。
這套方式既符合 DDD 的聚合邊界原則,又充分利用了 C# 的語言特性(強(qiáng)類型、record、依賴注入),是工業(yè)級(jí)項(xiàng)目中最常用的實(shí)踐方案。
本文使用 文章同步助手 同步