你是否正在使用ABP框架,却对AggregateRoot(聚合根)和FullAuditedEntity(完整审计实体)这两个基类的使用场景感到迷惑?它们听起来都像是实体类的“基类”,但实际上,它们服务于完全不同的设计目的,并且可以协同工作。这篇文章将为你清晰地拆解它们的核心定位、作用差异以及在实际项目中如何选择。
核心定位:先分清设计维度
首先需要明确的是,AggregateRoot和FullAuditedEntity并不互斥,它们是从不同维度对实体进行增强的基类。
AggregateRoot(聚合根):属于领域驱动设计(DDD) 范畴。它的核心是界定业务边界、管理聚合内子实体的生命周期,并确保复杂的业务规则在聚合内部保持一致。它是一个设计模式的应用。
FullAuditedEntity(完整审计实体):属于通用数据审计范畴。它的核心是自动记录实体的创建、修改、删除时间和操作人信息,是框架为方便开发而提供的“开箱即用”功能。
简单来说,一个关乎业务逻辑的组织方式,另一个关乎数据变化的追踪记录。
一、AggregateRoot(聚合根)详解
1. 核心作用
在领域驱动设计中,聚合是一组高度内聚的关联对象(实体和值对象)的集合,而聚合根就是这个集合的“根”和唯一对外访问点。它的核心职责包括:
- 作为聚合的唯一入口:外部世界(如应用服务)只能通过聚合根来访问或修改聚合内部的任何对象。这强制实施了业务边界。例如,
Order(订单)是聚合根,OrderItem(订单项)是其子实体。你不能凭空创建一个OrderItem并将其存入数据库,必须通过Order.AddItem(...)方法来操作。
- 保证业务规则一致性:聚合根封装了核心业务逻辑,确保在其边界内的所有状态变更都符合预设规则。例如,在
Order.Complete()方法中,可以校验所有订单项库存是否充足。
- 在ABP框架中的特殊待遇:这是关键且容易被忽略的一点。ABP会自动为任何直接或间接继承自
AggregateRoot<TKey>的类,注册其对应的泛型仓储接口IRepository<TEntity, TKey>。这意味着你可以在应用层直接注入IRepository<MyAggregateRoot, Guid>并使用,而对于普通实体(仅继承Entity<TKey>),则不会自动注册,你需要自己定义仓储。
2. 使用示例
下面的代码展示了一个简单的用户聚合根SysUser及其内部的子实体SysUserLoginLog。
using Volo.Abp.Domain.Entities;
// 聚合根:SysUser 作为用户聚合的根节点
public class SysUser : AggregateRoot<Guid>
{
// 聚合内的业务规则:设置用户名时不能为空
public string UserName
{
get => _userName;
private set => _userName = Check.NotNullOrWhiteSpace(value, nameof(UserName));
}
private string _userName;
// 聚合内的子实体集合,外部只能通过SysUser提供的方法访问
private List<SysUserLoginLog> _loginLogs = new();
public IReadOnlyList<SysUserLoginLog> LoginLogs => _loginLogs.AsReadOnly();
// 聚合根的业务方法:添加登录记录
public void AddLoginLog(string ipAddress)
{
_loginLogs.Add(new SysUserLoginLog(Guid.NewGuid(), Id, ipAddress, DateTime.Now));
}
// 必须有受保护的构造函数(供ABP仓储和ORM使用)
protected SysUser() { }
// 聚合根的创建方法
public SysUser(Guid id, string userName) : base(id)
{
UserName = userName;
}
}
// 聚合内的子实体:不能单独作为仓储操作
public class SysUserLoginLog : Entity<Guid>
{
public Guid UserId { get; }
public string IpAddress { get; }
public DateTime LoginTime { get; }
protected SysUserLoginLog() { }
public SysUserLoginLog(Guid id, Guid userId, string ipAddress, DateTime loginTime) : base(id)
{
UserId = userId;
IpAddress = ipAddress;
LoginTime = loginTime;
}
}
二、FullAuditedEntity(完整审计实体)详解
1. 核心作用
FullAuditedEntity<TKey>是ABP提供的一个功能丰富的基类,它自动为实体添加了一系列审计字段。框架的数据过滤器和拦截器会在数据持久化时自动填充这些字段的值,开发者无需在业务代码中手动处理。
它包含的审计字段如下:
| 字段名 |
类型 |
含义 |
CreationTime |
DateTime |
实体创建时间 |
CreatorId |
Guid? |
创建人ID(关联当前用户) |
LastModificationTime |
DateTime? |
最后修改时间 |
LastModifierId |
Guid? |
最后修改人ID |
IsDeleted |
bool |
是否软删除 |
DeletionTime |
DateTime? |
删除时间 |
DeleterId |
Guid? |
删除人ID |
ABP实际上提供了一套审计基类,方便你按需选择:
AuditedEntity<TKey>:仅包含创建和修改审计字段。
CreationAuditedEntity<TKey>:仅包含创建审计字段。
- 单独的接口如
IHasCreationTime,IHasModificationTime,IHasDeletionTime,可以组合实现。
2. 使用示例
using Volo.Abp;
using Volo.Abp.Domain.Entities.Auditing;
// 继承 FullAuditedEntity<Guid>,自动获得所有审计功能
public class SysRole : FullAuditedEntity<Guid>
{
public string Name { get; set; }
public string Code { get; set; }
protected SysRole() { }
public SysRole(Guid id, string name, string code) : base(id)
{
Name = Check.NotNullOrWhiteSpace(name, nameof(Name));
Code = Check.NotNullOrWhiteSpace(code, nameof(Code));
}
}
在应用服务中使用时,你完全不需要关心审计字段:
public async Task CreateRoleAsync(string name, string code)
{
var role = new SysRole(Guid.NewGuid(), name, code);
// ABP 会自动填充 CreationTime、CreatorId 等字段
await _roleRepository.InsertAsync(role);
}
三、两者如何结合使用?(实际开发中最常见)
在实际的.NET或ABP项目中,核心的业务实体通常既是聚合根,又需要审计功能。这可以通过多重继承实现。ABP框架非常贴心地为我们提供了预组合好的基类。
完整示例:既是聚合根又有完整审计的用户实体
using Volo.Abp;
using Volo.Abp.Domain.Entities.Auditing;
// 使用 ABP 提供的 FullAuditedAggregateRoot<TKey>,一步到位
public class SysUser : FullAuditedAggregateRoot<Guid>
{
public string UserName
{
get => _userName;
private set => _userName = Check.NotNullOrWhiteSpace(value, nameof(UserName));
}
private string _userName;
public string Email { get; private set; }
private List<SysUserLoginLog> _loginLogs = new();
public IReadOnlyList<SysUserLoginLog> LoginLogs => _loginLogs.AsReadOnly();
protected SysUser() { }
public SysUser(Guid id, string userName, string email) : base(id)
{
UserName = userName;
Email = Check.NotNullOrWhiteSpace(email, nameof(email));
}
public void UpdateEmail(string newEmail)
{
Email = Check.NotNullOrWhiteSpace(newEmail, nameof(newEmail));
// 无需手动更新 LastModificationTime,ABP 会自动处理
}
public void AddLoginLog(string ipAddress)
{
_loginLogs.Add(new SysUserLoginLog(Guid.NewGuid(), Id, ipAddress, DateTime.Now));
}
}
// 子实体也可以根据需要添加审计
public class SysUserLoginLog : CreationAuditedEntity<Guid>
{
public Guid UserId { get; }
public string IpAddress { get; }
public DateTime LoginTime { get; }
protected SysUserLoginLog() { }
public SysUserLoginLog(Guid id, Guid userId, string ipAddress, DateTime loginTime) : base(id)
{
UserId = userId;
IpAddress = ipAddress;
LoginTime = loginTime;
}
}
ABP提供的组合式聚合根基类:
| 基类名称 |
包含能力 |
AuditedAggregateRoot<TKey> |
聚合根 + 创建/修改审计 |
FullAuditedAggregateRoot<TKey> |
聚合根 + 完整审计(创建/修改/删除) |
CreationAuditedAggregateRoot<TKey> |
聚合根 + 创建审计 |
对于大多数需要软删除和完整操作追踪的核心业务实体,FullAuditedAggregateRoot<TKey>是最常用、最省事的选择。
总结与关键点回顾
为了更直观地对比,我们将核心差异总结如下表:
| 维度 |
AggregateRoot(聚合根) |
FullAuditedEntity(完整审计实体) |
| 设计范畴 |
领域驱动设计(DDD),关乎业务边界与规则 |
通用功能,关乎数据审计追踪 |
| 核心作用 |
1. 管理聚合内实体一致性 2. 保证业务规则 3. 自动注册仓储 |
自动记录创建、修改、删除的时间和操作人 |
| 继承关系 |
继承自 Entity<TKey> |
继承自 Entity<TKey>,可与聚合根结合 |
| 使用场景 |
作为业务聚合的根节点(如订单、用户、产品) |
几乎所有需要追踪“谁在何时做了什么”的实体 |
| ABP特殊待遇 |
自动拥有仓储 (IRepository<T>) |
自动填充审计字段,无需手动赋值 |
最后几个关键点帮你理清思路:
- 选择
AggregateRoot是基于业务设计,它决定了实体如何组织以及是否自动拥有仓储。选择FullAuditedEntity(或其变体)是基于功能需求,为了获得自动审计的便利。
- 实际开发中,核心业务实体通常继承
FullAuditedAggregateRoot<TKey>。这让你同时获得DDD聚合的设计优势和完备的审计功能,是ABP框架下的最佳实践之一。
- 只有聚合根才会被ABP自动注册仓储。如果你为一个普通实体(仅继承
Entity)注入IRepository,会导致运行时错误。子实体或值对象应通过其所属的聚合根进行操作。
希望这篇剖析能帮助你清晰地区分ABP中这两个重要的基类,并在你的下一个.NET项目中做出更合适的设计选择。如果你想深入探讨更多关于ABP或领域驱动设计的实践,欢迎到云栈社区与其他开发者交流。