聚合与强类型ID开发指南
什么是聚合根?
聚合根是 DDD 中的核心概念,代表一组相关对象的根实体,负责维护业务规则和数据一致性。在本模板中,所有聚合根都继承自 Entity<TId> 并实现 IAggregateRoot 接口。同时,本模板使用强类型ID提供类型安全,避免了不同实体ID之间的混淆。
聚合根文件应该放在哪里?
聚合根文件应遵循以下组织规则:
- 应放置在
src/{ProjectName}.Domain/AggregatesModel/{AggregateName}Aggregate/目录下 - 例如
src/MyProject.Domain/AggregatesModel/UserAggregate/User.cs - 每个聚合在独立文件夹中
- 聚合根类名与文件名一致
- 强类型ID与聚合根定义在同一文件中
如何定义强类型ID?
强类型ID的定义应遵循以下规则:
- 使用
IInt64StronglyTypedId或IGuidStronglyTypedId接口 - 使用
partial record声明,让框架生成具体实现 - 必须是public类型
- 与聚合/实体在同一个文件中定义
- 命名格式为
{EntityName}Id
如何定义聚合根?
聚合根的定义应遵循以下规则:
- 聚合内必须有一个且只有一个聚合根
- 命名不需要带后缀Aggregate
- 必须继承
Entity<TId>并实现IAggregateRoot接口 - 必须使用强类型ID,推荐使用
IGuidStronglyTypedId - 必须有 protected 无参构造器供 EF Core 使用
- 状态改变时发布领域事件,使用
this.AddDomainEvent() - 所有属性使用
private set,并显示设置默认值 - 无需手动设置ID的值
- RowVersion 属性用于乐观并发控制
如何定义子实体?
子实体的定义应遵循以下规则:
- 必须是
public类 - 必须有一个无参构造器
- 必须有一个强类型ID,推荐使用
IGuidStronglyTypedId - 必须继承自
Entity<TId>,并实现IEntity接口 - 聚合内允许多个子实体
如何编写聚合根代码示例?
基本聚合根示例
文件: src/MyProject.Domain/AggregatesModel/UserAggregate/User.cs
using MyProject.Domain.DomainEvents; // 必需:引用领域事件
namespace MyProject.Domain.AggregatesModel.UserAggregate;
// 强类型ID定义 - 与聚合根在同一文件中
public partial record UserId : IGuidStronglyTypedId;
public class User : Entity<UserId>, IAggregateRoot
{
protected User() { }
public User(string name, string email)
{
// 不手动设置ID,由EF Core值生成器自动生成
Name = name;
Email = email;
this.AddDomainEvent(new UserCreatedDomainEvent(this));
}
#region Properties
public string Name { get; private set; } = string.Empty;
public string Email { get; private set; } = string.Empty;
public RowVersion RowVersion { get; private set; } = new RowVersion(0);
#endregion
#region Methods
public void ChangeEmail(string email)
{
Email = email;
this.AddDomainEvent(new UserEmailChangedDomainEvent(this));
}
#endregion
}
如何编写带有子实体的聚合?
文件: src/MyProject.Domain/AggregatesModel/OrderAggregate/Order.cs
using MyProject.Domain.DomainEvents;
namespace MyProject.Domain.AggregatesModel.OrderAggregate;
// 订单ID
public partial record OrderId : IGuidStronglyTypedId;
// 订单项ID
public partial record OrderItemId : IGuidStronglyTypedId;
// 订单聚合根
public class Order : Entity<OrderId>, IAggregateRoot
{
protected Order() { }
private readonly List<OrderItem> _orderItems = new();
public Order(string customerName)
{
CustomerName = customerName;
OrderStatus = OrderStatus.Pending;
this.AddDomainEvent(new OrderCreatedDomainEvent(this));
}
#region Properties
public string CustomerName { get; private set; } = string.Empty;
public OrderStatus OrderStatus { get; private set; }
public IReadOnlyCollection<OrderItem> OrderItems => _orderItems.AsReadOnly();
public RowVersion RowVersion { get; private set; } = new RowVersion(0);
#endregion
#region Methods
public void AddItem(string productName, decimal price, int quantity)
{
var orderItem = new OrderItem(productName, price, quantity);
_orderItems.Add(orderItem);
this.AddDomainEvent(new OrderItemAddedDomainEvent(this, orderItem));
}
public void Confirm()
{
if (OrderStatus != OrderStatus.Pending)
{
throw new KnownException("只有待确认的订单才能被确认");
}
OrderStatus = OrderStatus.Confirmed;
this.AddDomainEvent(new OrderConfirmedDomainEvent(this));
}
#endregion
}
// 订单项 - 子实体
public class OrderItem : Entity<OrderItemId>, IEntity
{
protected OrderItem() { }
public OrderItem(string productName, decimal price, int quantity)
{
ProductName = productName;
Price = price;
Quantity = quantity;
}
public string ProductName { get; private set; } = string.Empty;
public decimal Price { get; private set; }
public int Quantity { get; private set; }
}
// 订单状态枚举
public enum OrderStatus
{
Pending,
Confirmed,
Shipped,
Completed,
Cancelled
}
遇到领域事件引用错误怎么办?
领域事件引用错误
错误: 未能找到类型或命名空间名"UserCreatedDomainEvent"
原因: 缺少对领域事件命名空间的引用
解决: 在聚合根文件顶部添加 using {ProjectName}.Domain.DomainEvents;
为什么会出现ID手动赋值错误?
错误: 手动在构造函数中设置ID值
原因: 违反了框架设计原则
解决: 移除ID赋值代码,让EF Core值生成器自动生成
为什么会出现缺少无参构造器的错误?
错误: EF Core无法实例化实体
原因: 缺少protected无参构造器
解决: 添加 protected EntityName() { } 构造器
聚合开发有哪些最佳实践?
- 业务逻辑封装: 将所有业务逻辑放在聚合根的方法中,而不是在外部操作属性
- 不变性保护: 使用
private set确保只能通过业务方法修改状态 - 领域事件发布: 在每个重要的状态变更时发布领域事件
- 强类型ID: 始终使用强类型ID,避免ID类型混淆
- 边界明确: 每个聚合应该有明确的业务边界,避免过大的聚合
- 子实体管理: 子实体应该通过聚合根的方法来添加、修改和删除