跳转到内容

实体

实体(Entity)是领域驱动设计中的核心概念之一。在 MiCake 中,实体是具有唯一标识的领域对象,通过 Id 进行识别和比较。

实体具有以下特征:

  1. 唯一标识:每个实体都有一个唯一的 Id 属性
  2. 可变性:实体的属性值可以改变,但标识不变
  3. 身份相等:两个实体相等当且仅当它们的 Id 相同
  4. 生命周期:实体有明确的生命周期,从创建到删除

MiCake 提供了 Entity<TKey> 基类用于定义实体:

using MiCake.DDD.Domain;
// 使用自定义 Key 类型
public class Customer : Entity<Guid>
{
public string Name { get; private set; }
public string Email { get; private set; }
public DateTime RegisterDate { get; private set; }
// 构造函数
private Customer() { } // EF Core 需要
// 在MiCake中推荐使用Create静态方法来创建领域对象
public static Customer Create(Guid id, string name, string email)
{
return new Customer
{
Id = id,
Name = name,
Email = email,
RegisterDate = DateTime.UtcNow
};
}
}

对于使用 int 类型作为主键的实体,可以使用简化版本:

// 默认使用 int 作为 Key 类型
public class Product : Entity
{
public string Name { get; private set; }
public decimal Price { get; private set; }
public int Stock { get; private set; }
private Product() { }
public Product(string name, decimal price, int stock)
{
Name = name;
Price = price;
Stock = stock;
}
}

实体的 Id 属性是其唯一标识:

public abstract class Entity<TKey> : IEntity<TKey> where TKey : notnull
{
public virtual TKey Id { get; init; } = default!;
}

特点:

  • 使用 init 访问器,确保 Id 在初始化后不可变
  • 可以在构造函数或对象初始化器中设置
  • 支持任何非空类型作为 Key
// int 类型(最常用)
public class Order : Entity<int> { }
// Guid 类型(分布式系统推荐)
public class Customer : Entity<Guid> { }
// long 类型(大数据量场景)
public class LogEntry : Entity<long> { }
// string 类型(业务编号)
public class Invoice : Entity<string> { }
// 方式一:工厂方法生成 (推荐)
public class Product : Entity<Guid>
{
private Product() { }
public static Product Create(string name, decimal price)
{
return new Product
{
Id = Guid.NewGuid(),
Name = name,
Price = price
};
}
}
// 方式二:构造时生成
public class Customer : Entity<Guid>
{
public Customer(string name)
{
Id = Guid.NewGuid(); // 在构造函数中生成
Name = name;
}
}
// 方式二:数据库自增
public class Order : Entity<int>
{
// Id 由数据库自增生成,无需手动设置
public Order(Customer customer)
{
Customer = customer;
}
}

MiCake 实体基类实现了基于标识的相等性比较:

var customer1 = new Customer(Guid.NewGuid(), "张三", "zhang@example.com");
var customer2 = new Customer(customer1.Id, "李四", "li@example.com");
// 只要 Id 相同,就认为是同一个实体
bool areEqual = customer1 == customer2; // true
// 即使属性值不同,但 Id 相同,仍然相等
Console.WriteLine(customer1.Name); // 张三
Console.WriteLine(customer2.Name); // 李四
Console.WriteLine(customer1 == customer2); // true

实体可以触发领域事件,用于捕获业务中发生的重要事件。

public class Order : Entity<int>
{
public OrderStatus Status { get; private set; }
public List<OrderItem> Items { get; private set; } = new();
public void AddItem(Product product, int quantity)
{
var item = new OrderItem(product, quantity);
Items.Add(item);
// 触发领域事件
RaiseDomainEvent(new OrderItemAddedEvent(Id, product.Id, quantity));
}
public void Submit()
{
if (Status != OrderStatus.Draft)
throw new DomainException("Only draft orders can be submitted");
Status = OrderStatus.Submitted;
// 触发提交事件
RaiseDomainEvent(new OrderSubmittedEvent(Id, Items.Sum(i => i.TotalPrice)));
}
}
// 获取实体的所有待处理事件
IReadOnlyCollection<IDomainEvent> events = order.DomainEvents;
// 清除所有事件(通常由框架自动调用)
order.ClearDomainEvents();

事件会在调用 SaveChangesAsync() 时自动派发,详见领域事件章节。

实体应该包含相关的业务逻辑,而不仅仅是数据容器:

public class BankAccount : Entity<Guid>
{
public decimal Balance { get; private set; }
public AccountStatus Status { get; private set; }
private BankAccount() { }
public static BankAccount Open(Guid id, decimal initialDeposit)
{
if (initialDeposit < 0)
throw new DomainException("Initial deposit cannot be negative");
var account = new BankAccount
{
Id = id,
Balance = initialDeposit,
Status = AccountStatus.Active
};
account.RaiseDomainEvent(new AccountOpenedEvent(id, initialDeposit));
return account;
}
public void Deposit(decimal amount)
{
if (amount <= 0)
throw new DomainException("Deposit amount must be positive");
if (Status != AccountStatus.Active)
throw new DomainException("Account is not active");
Balance += amount;
RaiseDomainEvent(new MoneyDepositedEvent(Id, amount, Balance));
}
public void Withdraw(decimal amount)
{
if (amount <= 0)
throw new DomainException("Withdrawal amount must be positive");
if (Status != AccountStatus.Active)
throw new DomainException("Account is not active");
if (Balance < amount)
throw new DomainException("Insufficient balance");
Balance -= amount;
RaiseDomainEvent(new MoneyWithdrawnEvent(Id, amount, Balance));
}
public void Close()
{
if (Balance != 0)
throw new DomainException("Cannot close account with non-zero balance");
Status = AccountStatus.Closed;
RaiseDomainEvent(new AccountClosedEvent(Id));
}
}
  1. 使用私有 setter:防止外部直接修改属性
  2. 验证业务规则:在方法中验证业务约束
  3. 抛出领域异常:使用 DomainException 报告业务错误
  4. 触发领域事件(可选):记录重要的业务事件
  5. 保持状态一致:确保实体在方法执行后处于有效状态

普通实体没有独立的生命周期,必须属于某个聚合:

// OrderItem 是普通实体,不是聚合根
public class OrderItem : Entity<int>
{
public int OrderId { get; private set; }
public int ProductId { get; private set; }
public int Quantity { get; private set; }
public decimal Price { get; private set; }
// 只能通过 Order 聚合根访问和修改
}

聚合根是特殊的实体,可以独立存在并作为聚合的入口:

// Order 是聚合根
public class Order : AggregateRoot<int>
{
private List<OrderItem> _items = new();
public IReadOnlyCollection<OrderItem> Items => _items.AsReadOnly();
public void AddItem(Product product, int quantity, decimal price)
{
var item = new OrderItem
{
ProductId = product.Id,
Quantity = quantity,
Price = price
};
_items.Add(item);
}
// 所有对 OrderItem 的操作都通过 Order
}

关键区别:

  • 普通实体:Entity<TKey>
  • 聚合根:AggregateRoot<TKey>(继承自 Entity<TKey>
  • 只有聚合根可以有仓储
  • 普通实体只能通过聚合根访问

详见聚合根章节。

public class Customer : Entity<Guid>
{
private Customer() { }
// 工厂方法确保实体创建时满足业务规则
public static Customer Create(string name, string email)
{
if (string.IsNullOrWhiteSpace(name))
throw new DomainException("Customer name is required");
if (!email.Contains("@"))
throw new DomainException("Invalid email format");
return new Customer
{
Id = Guid.NewGuid(),
Name = name,
Email = email,
RegisterDate = DateTime.UtcNow
};
}
}
public class Order : Entity<int>
{
private List<OrderItem> _items = new();
private decimal _totalAmount;
public void AddItem(OrderItem item)
{
_items.Add(item);
// 立即更新总金额,保持一致性
_totalAmount += item.TotalPrice;
}
public void RemoveItem(OrderItem item)
{
if (_items.Remove(item))
{
_totalAmount -= item.TotalPrice;
}
}
}
public class Product : Entity<int>
{
// 使用 private set 防止外部修改
public string Name { get; private set; }
public decimal Price { get; private set; }
// 通过方法修改,可以加入验证逻辑
public void UpdatePrice(decimal newPrice)
{
if (newPrice < 0)
throw new DomainException("Price cannot be negative");
Price = newPrice;
}
}
public class ShoppingCart : Entity<Guid>
{
private List<CartItem> _items = new();
private const int MaxItemsCount = 100;
public void AddItem(Product product, int quantity)
{
// 业务规则验证
if (quantity <= 0)
throw new DomainException("Quantity must be positive");
if (_items.Count >= MaxItemsCount)
throw new DomainException("Cart is full");
if (product.Stock < quantity)
throw new DomainException("Insufficient stock");
// 执行操作
_items.Add(new CartItem(product, quantity));
}
}
public class User : Entity<int>
{
public string Email { get; private set; }
public bool IsEmailVerified { get; private set; }
public void VerifyEmail(string verificationCode)
{
// 验证逻辑
if (IsEmailVerified)
throw new DomainException("Email already verified");
// 验证 code...
IsEmailVerified = true;
// 触发事件,让其他部分响应(如发送欢迎邮件)
RaiseDomainEvent(new EmailVerifiedEvent(Id, Email));
}
}
// 错误:实体只有数据,没有行为
public class Order : Entity<int>
{
public int CustomerId { get; set; }
public OrderStatus Status { get; set; }
public decimal TotalAmount { get; set; }
}
// 业务逻辑在外部服务中
public class OrderService
{
public void SubmitOrder(Order order)
{
order.Status = OrderStatus.Submitted;
// ...
}
}
// 正确:实体包含业务逻辑
public class Order : Entity<int>
{
public int CustomerId { get; private set; }
public OrderStatus Status { get; private set; }
public decimal TotalAmount { get; private set; }
public void Submit()
{
if (Status != OrderStatus.Draft)
throw new DomainException("Only draft orders can be submitted");
Status = OrderStatus.Submitted;
RaiseDomainEvent(new OrderSubmittedEvent(Id));
}
}

实体是 DDD 中的基础概念,在 MiCake 中:

  • 继承 Entity<TKey>Entity 基类
  • 通过唯一的 Id 标识
  • 可以触发领域事件
  • 应该包含相关的业务逻辑
  • 使用私有 setter 保护数据
  • 通过方法修改状态并验证业务规则

下一步: