实体继承的设计
ABP 的实体体系不是一次设计出 7 层的,而是逐层递增,每个层次解决一个问题。
源码在 framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/,看每个基类实际加了什么。
每一层的源码分析
第 1 层:IEntity / Entity
csharp
// IEntity.cs
public interface IEntity
{
object?[] GetKeys(); // 返回实体的所有主键
}
最基本的实体接口。只定义了"获取主键",不指定主键类型。复合主键返回多个值,单主键返回一个值。
csharp
// Entity.cs
public abstract class Entity<TKey> : Entity, IEntity<TKey>
{
public virtual TKey Id { get; protected set; } = default!;
public override object?[] GetKeys() => [Id];
}
第 2 层:BasicAggregateRoot(聚合根)
csharp
// BasicAggregateRoot.cs
public abstract class BasicAggregateRoot<TKey> : Entity<TKey>, IAggregateRoot, IGeneratesDomainEvents
{
private ICollection<DomainEventRecord>? _distributedEvents;
private ICollection<DomainEventRecord>? _localEvents;
// 提供领域事件的支持
protected virtual void AddLocalEvent(object eventData) { ... }
protected virtual void AddDistributedEvent(object eventData) { ... }
}
聚合根和实体的区别:聚合根可以产生领域事件,是 DDD 中数据一致性的边界。只有聚合根可以有仓储。
第 3 层:AggregateRoot(扩展属性 + 并发戳)
csharp
// AggregateRoot.cs
public abstract class AggregateRoot<TKey> : BasicAggregateRoot<TKey>,
IHasExtraProperties, IHasConcurrencyStamp
{
public virtual ExtraPropertyDictionary ExtraProperties { get; protected set; }
public virtual string ConcurrencyStamp { get; set; }
protected AggregateRoot()
{
ConcurrencyStamp = Guid.NewGuid().ToString("N");
ExtraProperties = new ExtraPropertyDictionary();
}
}
ExtraProperties 是字典,可以随时添加自定义字段而不需要改数据库表结构。ConcurrencyStamp 用于乐观锁并发控制。
第 4-6 层:审计实体
csharp
// CreationAudited:增加创建审计
// CreationTime + CreatorId
// Audited:增加修改审计
// + LastModificationTime + LastModifierId
// FullAudited:增加软删除审计
// + IsDeleted + DeleterId + DeletionTime
这三个基类对应接口 ICreationAuditedObject → IAuditedObject → IFullAuditedObject,逐层继承:
csharp
public interface IFullAuditedObject : IAuditedObject, IDeletionAuditedObject { }
public interface IAuditedObject : ICreationAuditedObject, IModificationAuditedObject { }
尝试理解源码的关键设计哲学
为什么 Entity
和 AggregateRoot 分开?
因为不是所有实体都是聚合根。DDD 中聚合根是数据一致性的边界,只有聚合根才能拥有仓储。如果所有实体都用 AggregateRoot,就无法区分哪些实体是独立数据入口、哪些是子实体。
为什么 ConcurrencyStamp 自动生成 GUID?
csharp
ConcurrencyStamp = Guid.NewGuid().ToString("N");
在 AggregateRoot 构造函数中自动生成,而不是让开发者手动设置。当 EF Core 更新实体时,会比较数据库中的 ConcurrencyStamp 和当前值,如果不一致说明数据被其他用户修改了,抛出 DbUpdateConcurrencyException。
ExtraProperties 的底层原理
csharp
public virtual ExtraPropertyDictionary ExtraProperties { get; protected set; }
这个字典在保存到数据库时,通过值转换器(Value Converter)序列化为 JSON 字符串存入一个字段。查询时反序列化回来。这样不用增加数据库列,就能动态添加属性。
实战:如何选择合适的基类
csharp
// 场景 1:字典表,不需要审计
public class Tag : Entity<int> { } // 只有 Id,最轻量
// 场景 2:业务主表,需要谁创建的
public class Article : CreationAuditedAggregateRoot<Guid> { }
// 自带 CreationTime, CreatorId
// 场景 3:核心数据,需要完整审计 + 软删除
public class Order : FullAuditedAggregateRoot<Guid> { }
// 自带 7 个审计字段,删除时只标记不物理删除
// 场景 4:需要扩展属性但不需要审计
public class Product : AggregateRoot<Guid> { }
// 有 ExtraProperties + ConcurrencyStamp
原则:够用就好。不需要软删除就不要用 FullAudited,避免不必要的字段和查询条件。自己在开发中要好好学习这个思想。