ABP框架DDD实现中增改聚合根时DomainManager的正确用法
ABP框架DDD落地:实体创建/更新逻辑复用实现方案
核心问题说明
你当前代码运行异常、逻辑不符合DDD规范的核心原因是错误地将创建实体的工厂方法CreateAsync用在了更新场景:工厂方法的唯一职责是生成全新的聚合根实例,新实例会生成独立主键、默认状态值,直接将新实例映射覆盖已持久化的旧实体,必然会出现主键冲突、审计字段(创建时间/创建人)丢失、并发令牌失效、内置状态被覆盖等问题。
符合DDD规范的实现逻辑遵循以下原则:
- 领域服务(即你定义的
AporteManager)承载核心业务规则,公共逻辑抽离复用,不跨创建/更新场景混用职责单一的方法 - 聚合根实体自身封装状态变更逻辑,不对外暴露公开属性setter,保证实体永远处于合法状态
- 应用层仅做流程编排,不编写任何业务逻辑,避免规则散落在不同层重复维护
分步实现代码
1. 重构Domain层AporteManager领域服务
首先抽离创建、更新场景共用的业务逻辑,分别实现职责单一的创建、更新领域方法:
public class AporteManager : DomainService { private readonly IContaBancariaRepository _contaBancariaRepository; public AporteManager(IContaBancariaRepository contaBancariaRepository) { _contaBancariaRepository = contaBancariaRepository; } /// <summary> /// 公共逻辑:补全关联字段、做合法性校验,创建/更新场景复用 /// </summary> private async Task<AporteValidation> ProcessarValidacaoAsync(AporteValidation valid) { var contasBancarias = await _contaBancariaRepository.WithDetailsAsync( x => x.Banco, x => x.FonteDeRecurso, x => x.CodigoDeAplicacao, x => x.Fundo, x => x.FonteDeRecursoSICONFI ); var contaSelecionada = contasBancarias.FirstOrDefault(x => x.Id == valid.ContaBancariaId); // 领域校验直接在领域层抛出异常,不要下沉到应用层判断 if (contaSelecionada == null) { throw new BusinessException("Contabilidade:001", "指定的银行账户不存在"); } valid.FonteDeRecursoId = contaSelecionada.FonteDeRecurso?.Id; valid.CodigoDeAplicacaoId = contaSelecionada.CodigoDeAplicacao?.Id; return valid; } /// <summary> /// 工厂方法:仅负责全新Aporte实体的创建 /// </summary> public async Task<Aporte> CreateAsync(AporteValidation valid) { var validacaoProcessada = await ProcessarValidacaoAsync(valid); // 此处可追加创建场景独有的业务规则:比如编号重复校验、初始状态设置等 return new Aporte(validacaoProcessada); } /// <summary> /// 领域更新方法:负责已有Aporte实体的变更逻辑 /// </summary> public async Task UpdateAsync(Aporte entidadeExistente, AporteValidation valid) { var validacaoProcessada = await ProcessarValidacaoAsync(valid); // 调用实体自身的更新方法完成属性变更,所有状态校验内置在实体内部 entidadeExistente.AtualizarDados(validacaoProcessada); // 此处可追加更新场景独有的业务规则:比如已审核的实体不允许修改核心字段等 } }
2. 完善Domain层Aporte聚合根
DDD聚合根的核心要求是:所有状态变更必须通过实体自身的方法完成,禁止外部直接修改实体属性,保证实体永远不会处于非法状态。
public class Aporte : AggregateRoot<int> { // 私有构造函数供ORM序列化使用,外部只能通过工厂方法创建实体 private Aporte() { } /// <summary> /// 构造函数:供工厂方法创建新实体时调用 /// </summary> internal Aporte(AporteValidation valid) { SetarDadosBasicos(valid); // 初始化创建场景的业务默认值,比如实体状态、流水号生成等 } /// <summary> /// 内部公共赋值方法:创建/更新场景复用,避免重复写属性赋值逻辑 /// </summary> private void SetarDadosBasicos(AporteValidation valid) { ContaBancariaId = valid.ContaBancariaId; FonteDeRecursoId = valid.FonteDeRecursoId; CodigoDeAplicacaoId = valid.CodigoDeAplicacaoId; // 其余可修改的业务字段统一在此处赋值 } /// <summary> /// 实体对外暴露的更新方法,控制访问权限,避免外部绕过校验直接修改属性 /// </summary> internal void AtualizarDados(AporteValidation valid) { // 此处可追加实体状态校验:比如已审核、已作废的实体不允许修改 // if (Status == StatusAporte.Aprovado) throw new BusinessException("Contabilidade:002", "已审核的入款记录不允许修改"); SetarDadosBasicos(valid); } // 所有业务属性使用protected/private set,禁止外部直接赋值 public int ContaBancariaId { get; protected set; } public Guid? FonteDeRecursoId { get; protected set; } public Guid? CodigoDeAplicacaoId { get; protected set; } // 其余业务属性定义 }
3. 简化Application层UpdateAsync方法
应用层只做流程编排,不承载任何业务逻辑:
[Authorize(ContabilidadePermissions.Aporte.Edit)] public async Task UpdateAsync(int id, CreateUpdateAporteDto input) { // ABP仓储的GetAsync默认会在实体不存在时自动抛出EntityNotFoundException,无需手动判断null var aporte = await _aporteRepository.GetAsync(id); // 仅做DTO和领域参数对象的映射,不做业务处理 var validation = ObjectMapper.Map<CreateUpdateAporteDto, AporteValidation>(input); // 调用领域服务完成更新逻辑,所有业务规则全部下沉到领域层 await _aporteManager.UpdateAsync(aporte, validation); // ABP工作单元会自动跟踪实体变更并持久化,手动调用UpdateAsync也可兼容 await _aporteRepository.UpdateAsync(aporte); }
原有代码的其他问题修正
- 移除应用层的大段
try-catch统一异常包装逻辑:这种写法会掩盖真实错误(比如关联数据不存在、字段校验失败、数据库约束冲突),极大增加排查难度。领域层抛出的BusinessException会被ABP框架自动处理为友好的接口返回,无需额外包装。 - 移除更新场景中"创建新实体再映射覆盖旧实体"的逻辑:这种写法完全违背了ORM的变更跟踪逻辑和DDD的实体生命周期管理规则。
- 所有业务规则只在领域层维护一份:后续规则变更时不需要同时修改应用层、领域层的多份代码,降低维护成本。
内容的提问来源于stack exchange,提问作者Heitor Giacomini
相关产品推荐
相关产品推荐

