Automapper ForAllPropertyMaps()谓词存在意外副作用问题排查
AutoMapper 11.0.1 中 ForAllPropertyMaps 注册解析器的异常问题排查
问题场景
在.NET 6控制台应用中使用AutoMapper 11.0.1,通过ForAllPropertyMaps()注册自定义解析器,核心逻辑是检测源端Nullable类型属性:当源属性HasValue为false时,返回目标属性原有值以保留原值。但发现仅注释掉获取pm.SourceType的代码(谓词逻辑始终返回true,未做任何功能修改),映射行为就出现异常:目标属性被设置为default(T)而非保留原值。
完整演示代码
using AutoMapper; using System; namespace AutoMapperNullableIssue { class Source { public int? NullableInt { get; set; } public string StringProp { get; set; } } class Destination { public int NullableInt { get; set; } public string StringProp { get; set; } } class NullablePreserveResolver<TSource, TDestination, TMember> : IValueResolver<TSource, TDestination, TMember> { public TMember Resolve(TSource source, TDestination destination, TMember destMember, ResolutionContext context) { var sourceProp = context.MemberMap.SourceMember; if (sourceProp.PropertyType.IsGenericType && sourceProp.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) { var hasValue = (bool)sourceProp.PropertyType.GetProperty("HasValue").GetValue(source); if (!hasValue) { return destMember; } } return (TMember)sourceProp.GetValue(source); } } class Program { static void Main(string[] args) { // 工作场景配置:保留pm.SourceType访问 var workingConfig = new MapperConfiguration(cfg => { cfg.CreateMap<Source, Destination>(); cfg.ForAllPropertyMaps(pm => { var type = pm.SourceType; return true; }, (pm, c) => c.ResolveUsing<NullablePreserveResolver<Source, Destination, object>>()); }); // 失败场景配置:注释掉pm.SourceType访问 var failingConfig = new MapperConfiguration(cfg => { cfg.CreateMap<Source, Destination>(); cfg.ForAllPropertyMaps(pm => { // var type = pm.SourceType; return true; }, (pm, c) => c.ResolveUsing<NullablePreserveResolver<Source, Destination, object>>()); }); // 测试工作场景 var workingMapper = workingConfig.CreateMapper(); var destWorking = new Destination { NullableInt = 10, StringProp = "Original" }; var source = new Source { NullableInt = null, StringProp = "New" }; workingMapper.Map(source, destWorking); Console.WriteLine($"Working: NullableInt={destWorking.NullableInt}, StringProp={destWorking.StringProp}"); // 输出:Working: NullableInt=10, StringProp=New // 测试失败场景 var failingMapper = failingConfig.CreateMapper(); var destFailing = new Destination { NullableInt = 10, StringProp = "Original" }; failingMapper.Map(source, destFailing); Console.WriteLine($"Failing: NullableInt={destFailing.NullableInt}, StringProp={destFailing.StringProp}"); // 输出:Failing: NullableInt=0, StringProp=New } } }
工作/失败场景的表达式树对比
两段表达式树代码文本完全一致,但实际生成的映射逻辑存在底层差异:
.ResolveUsing((Source src, Destination dest, int destMember, ResolutionContext ctx) => { var sourceProp = ctx.MemberMap.SourceMember; if (sourceProp.PropertyType.IsGenericType && sourceProp.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>)) { var hasValue = (bool)sourceProp.PropertyType.GetProperty("HasValue").GetValue(src); if (!hasValue) { return destMember; } } return (int)sourceProp.GetValue(src); })
问题原因分析
这是AutoMapper 11.x版本的内部优化逻辑导致的冲突:
- 当谓词表达式**访问了
PropertyMap的成员(如SourceType)**时,AutoMapper会标记该谓词为"需要延迟计算",不会提前缓存结果,而是在每个属性映射时重新执行,确保解析器能正确绑定当前属性的上下文,包括目标对象的原有值。 - 当谓词表达式未访问
PropertyMap的任何成员(仅返回true),AutoMapper会触发缓存优化,提前将谓词结果缓存为true,但在此过程中错误地将解析器的destMember参数绑定为default(T),而非实际的目标对象原有属性值。
解决方案
有三种可行的解决方式:
保留
PropertyMap成员访问:即使不需要使用pm.SourceType,也可以添加无意义的访问语句,强制AutoMapper不触发缓存优化:cfg.ForAllPropertyMaps(pm => { _ = pm.SourceType; // 保留该语句,避免优化逻辑触发 return true; }, (pm, c) => c.ResolveUsing<NullablePreserveResolver<Source, Destination, object>>());改用
ForAllMaps替代ForAllPropertyMaps:ForAllMaps的执行逻辑不会触发上述缓存优化,可确保解析器正确获取目标原有值:cfg.ForAllMaps((map, exp) => { foreach (var propMap in map.PropertyMaps) { exp.ForMember(propMap.DestinationMember.Name, opt => opt.ResolveUsing<NullablePreserveResolver<Source, Destination, object>>()); } });升级AutoMapper版本:该问题在AutoMapper 12.x及以后的版本中已被修复,升级到最新稳定版可直接解决此问题。
内容的提问来源于stack exchange,提问作者redcalx
相关产品推荐
相关产品推荐

