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

C#中使用Manager类实现实体对象强制校验的最佳方案

最优实现方案

解决入参过多重构成本高的问题

不要使用零散参数作为创建方法的入参,在领域层定义专门的不可变参数对象承载所有创建实体需要的字段,后续字段调整只需要修改参数对象和实体内部赋值逻辑,不需要修改工厂方法签名,大幅降低重构成本:

// 和Precatorio实体放在同一领域层命名空间
public record PrecatorioCreateData(
    int FornecedorId,
    Fornecedor Fornecedor,
    int FundoId,
    Fundo Fundo,
    int DestinacaoRecursoId,
    DestinacaoRecurso DestinacaoRecurso,
    DateTime Data,
    int Natureza,
    long Tipo,
    decimal Valor,
    string Descricao
);

接口层的CreateUpdatePrecatorioDto只需要配置映射到PrecatorioCreateData即可,不要直接映射到实体类型。

强制所有实体实例化经过Manager校验

从语法限制、流程校验两个层面堵死所有绕过校验的路径:

  1. 收紧实体访问权限
    把Precatorio的所有属性set访问器改为private init,禁止外部直接修改属性;把所有构造函数改为private,其中无参构造保留给EF Core查询时使用(EF Core原生支持调用私有无参构造实例化从数据库读取的实体,这部分数据是已经持久化的合法数据,不需要重复校验)。
    把PrecatorioManager改为Precatorio的嵌套类,C#语法中嵌套类可以访问外部类的私有成员,这样Manager可以正常调用私有构造函数创建实例,但外部任何类(包括同程序集的其他类)都无法直接new实体、也无法直接给实体属性赋值。
    改造后的实体和Manager代码如下:
    public class Precatorio : Pesquisa.Pesquisa
    { 
        public int FornecedorId { get; private init; }
        public Fornecedor Fornecedor { get; private init; }
        public int FundoId { get; private init; }
        public Fundo Fundo { get; private init; }
        public int DestinacaoRecursoId { get; private init; }
        public DestinacaoRecurso DestinacaoRecurso { get; private init; }
        public DateTime Data { get; private init; }
        public int Natureza { get; private init; }
        public long Tipo { get; private init; }
        public decimal Valor { get; private init; }
        public string Descricao { get; private init; }
    
        // EF Core 反序列化使用的私有构造
        private Precatorio()
        { 
        }
    
        // 实体实例化的唯一构造入口,仅嵌套类可访问
        private Precatorio(PrecatorioCreateData data)
        {   
            FornecedorId = data.FornecedorId;
            Fornecedor = data.Fornecedor;
            FundoId = data.FundoId;
            Fundo = data.Fundo;
            DestinacaoRecursoId = data.DestinacaoRecursoId;
            DestinacaoRecurso = data.DestinacaoRecurso;
            Data = data.Data;
            Natureza = data.Natureza;
            Tipo = data.Tipo;
            Valor = data.Valor;
            Descricao = data.Descricao;
        }
    
        // 嵌套的领域服务,作为创建实体的唯一合法入口
        public class Manager : DomainService
        {
            private readonly IPrecatorioRepository _precatorioRepository;
    
            public Manager(IPrecatorioRepository precatorioRepository)
            {
                _precatorioRepository = precatorioRepository;
            }
    
            public async Task<Precatorio> CreateAsync(PrecatorioCreateData input)
            {
                // 所有字段合法性校验、依赖仓储的业务校验全部写在这里
                if (input.Valor <= 0)
                    throw new ArgumentException("金额不能小于等于0", nameof(input.Valor));
                if (string.IsNullOrWhiteSpace(input.Descricao))
                    throw new ArgumentException("描述不能为空", nameof(input.Descricao));
                if (await _precatorioRepository.ExistsAsync(x => 
                    x.FornecedorId == input.FornecedorId 
                    && x.Data == input.Data 
                    && x.Tipo == input.Tipo))
                {
                    throw new InvalidOperationException("已存在相同的催付凭证记录");
                }
    
                // 校验通过后才调用私有构造返回合法实例
                return new Precatorio(input);
            }
        }
    }
    
  2. 堵死对象映射绕过路径
    由于改造后的Precatorio没有公开构造函数、没有公开可写属性,主流对象映射框架(如AutoMapper)默认无法直接映射生成Precatorio实例。为了避免开发人员手动配置映射绕过限制,可以在应用启动时增加映射配置校验:扫描所有对象映射规则,只要存在目标类型为Precatorio的映射配置,直接抛出异常阻止应用启动,从流程上杜绝直接映射到实体的可能。
  3. 更新场景的校验补充
    针对实体更新场景,不要公开属性setter,而是在Precatorio实体内部定义对应的业务方法(如UpdateValor(decimal novoValor)),方法内部先完成参数合法性校验再修改属性值,避免外部直接修改属性绕过业务规则。

该方案完全符合DDD领域实体的设计规范,从语法层面强制所有新建实体必须经过Manager校验,同时通过参数对象把方法签名的重构成本降到最低。

内容的提问来源于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 22:45:38