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校验
从语法限制、流程校验两个层面堵死所有绕过校验的路径:
- 收紧实体访问权限
把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); } } } - 堵死对象映射绕过路径
由于改造后的Precatorio没有公开构造函数、没有公开可写属性,主流对象映射框架(如AutoMapper)默认无法直接映射生成Precatorio实例。为了避免开发人员手动配置映射绕过限制,可以在应用启动时增加映射配置校验:扫描所有对象映射规则,只要存在目标类型为Precatorio的映射配置,直接抛出异常阻止应用启动,从流程上杜绝直接映射到实体的可能。 - 更新场景的校验补充
针对实体更新场景,不要公开属性setter,而是在Precatorio实体内部定义对应的业务方法(如UpdateValor(decimal novoValor)),方法内部先完成参数合法性校验再修改属性值,避免外部直接修改属性绕过业务规则。
该方案完全符合DDD领域实体的设计规范,从语法层面强制所有新建实体必须经过Manager校验,同时通过参数对象把方法签名的重构成本降到最低。
内容的提问来源于stack exchange,提问作者Heitor Giacomini
相关产品推荐
相关产品推荐

