You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 16:48:46