ABP DDD 实体源码解读:7 层基类的设计逻辑和适用场景

实体继承的设计

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

这三个基类对应接口 ICreationAuditedObjectIAuditedObjectIFullAuditedObject,逐层继承:

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,避免不必要的字段和查询条件。自己在开发中要好好学习这个思想。

相关推荐
硅基喵2 天前
ABP 模块系统源码学习:启动时模块是如何加载和排序的
dotnet
硅基喵3 个月前
C# 也能像 Python 一样写脚本 | .NET 10 构建基于文件的应用
dotnet
硅基喵3 个月前
.NET 10 使用 Microsoft.AspNetCore.OpenApi 实现 API 版本管理
dotnet
硅基喵4 个月前
ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑
dotnet
ChaITSimpleLove5 个月前
aiagent-webapi 命令的详细使用说明
dotnet·webapi·ai agent·agent framework·maf·projecttemp
TeamDev5 个月前
使用 Docker 部署 DotNetBrowser 应用程序
运维·ui·docker·容器·桌面应用·dotnet·dotnetbrowser
CSharp精选营5 个月前
.NET命名之谜:它与C#纠缠20年的关系揭秘
c#·.net·dotnet·csharp
VAllen5 个月前
ConcurrentNativeQueue<T>:一个使用 .NET 实现的零 GC 压力的无锁 MPSC 原生队列
c#·.net·性能测试·.net core·dotnet·csharp
lindexi6 个月前
dotnet Vortice 通过 Angle 将 Skia 和 DirectX 对接
dotnet