惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
腾讯CDC
宝玉的分享
宝玉的分享
量子位
Recent Announcements
Recent Announcements
Martin Fowler
Martin Fowler
J
Java Code Geeks
V
Visual Studio Blog
阮一峰的网络日志
阮一峰的网络日志
Blog — PlanetScale
Blog — PlanetScale
大猫的无限游戏
大猫的无限游戏
博客园 - 叶小钗
S
SegmentFault 最新的问题
B
Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 【当耐特】
小众软件
小众软件
The Cloudflare Blog
Y
Y Combinator Blog
I
InfoQ
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
IT之家
IT之家

博客园 - 邓磊Lei

我可以双击 shift 打开 visual studio 的代码搜索 我决定把 gittoy 更名为 git blame for vs 为了可以重新拥抱 Visual Studio, 我做了一个 vs 版的 gittoolbox ABP DTO 与对象映射:AutoMapper Profile 配置与 Mapperly 源生成器 ABP 应用服务源码解读:ApplicationService 基类内置了哪些能力 ABP 仓储模式源码解读:自动过滤是如何实现的 ABP 模块系统源码学习:启动时模块是如何加载和排序的 为什么 Go 一个 HTTP 服务可以同时处理数万连接 gin怎么处理一次http请求的 我的经验:git提交信息分别用什么icon C# 也能像 Python 一样写脚本 | .NET 10 构建基于文件的应用 .NET 10 使用 Microsoft.AspNetCore.OpenApi 实现 API 版本管理 ASP.NET Core 内存缓存实战:一篇搞懂该怎么配、怎么避坑 Microsoft Agent Framework + Kimi API 实战:控制台应用跑通单次与多轮 Agent 对话 开发实战:asp.net core + ef core 实现动态可扩展的分页方案 聊聊 ASP.NET Core 中间件和过滤器的区别 Python 入门:从“其他语言”到 Pythonic 思维的完整迁移手册 .NET 进阶之路:异步、并发与内存管理的系统性认知 Redis:延迟双删的适用边界与落地细节 Serilog:从结构化日志认知到 .NET 工程落地 EF Core 原生 SQL 实战:FromSql、SqlQuery 与对象映射边界 EF Core 拦截器实战:SaveChangesInterceptor、CommandInterceptor 与审计落地 ASP.NET Core 外部依赖调用治理实战:HttpClientFactory、Polly 与幂等边界 .NET .Result 避坑指南:不同框架下的死锁与线程池饥饿 EF Core 慢查询排查实战:TagWith、OpenTelemetry、执行计划,30 分钟定位性能瓶颈 EF Core 并发冲突实战:乐观锁、RowVersion 与 DbUpdateConcurrencyException 怎么处理 EF Core 写入链路深拆:从 ChangeTracker 到 SQL Batch 的性能诊断与优化 ASP.NET Core 认证鉴权实战:JWT、Policy 与权限边界怎么落地 C# 异步编程深水区:Task、ValueTask、线程池饥饿与背压设计 EF Core 查询性能黑洞:Include、投影与跟踪策略的边界
ABP DDD 实体源码解读:7 层基类的设计逻辑和适用场景
DLForge · 2026-08-03 · via 博客园 - 邓磊Lei

实体继承的设计

ABP 的实体体系不是一次设计出 7 层的,而是逐层递增,每个层次解决一个问题。

源码在 framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/,看每个基类实际加了什么。

每一层的源码分析

第 1 层:IEntity / Entity

// IEntity.cs
public interface IEntity
{
    object?[] GetKeys();  // 返回实体的所有主键
}

最基本的实体接口。只定义了"获取主键",不指定主键类型。复合主键返回多个值,单主键返回一个值。

// 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(聚合根)

// 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(扩展属性 + 并发戳)

// 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 层:审计实体

// CreationAudited:增加创建审计
// CreationTime + CreatorId

// Audited:增加修改审计
// + LastModificationTime + LastModifierId

// FullAudited:增加软删除审计
// + IsDeleted + DeleterId + DeletionTime

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

public interface IFullAuditedObject : IAuditedObject, IDeletionAuditedObject { }
public interface IAuditedObject : ICreationAuditedObject, IModificationAuditedObject { }

尝试理解源码的关键设计哲学

为什么 Entity 和 AggregateRoot 分开?

因为不是所有实体都是聚合根。DDD 中聚合根是数据一致性的边界,只有聚合根才能拥有仓储。如果所有实体都用 AggregateRoot,就无法区分哪些实体是独立数据入口、哪些是子实体。

为什么 ConcurrencyStamp 自动生成 GUID?

ConcurrencyStamp = Guid.NewGuid().ToString("N");

AggregateRoot 构造函数中自动生成,而不是让开发者手动设置。当 EF Core 更新实体时,会比较数据库中的 ConcurrencyStamp 和当前值,如果不一致说明数据被其他用户修改了,抛出 DbUpdateConcurrencyException

public virtual ExtraPropertyDictionary ExtraProperties { get; protected set; }

这个字典在保存到数据库时,通过值转换器(Value Converter)序列化为 JSON 字符串存入一个字段。查询时反序列化回来。这样不用增加数据库列,就能动态添加属性。

实战:如何选择合适的基类

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