AutoMapper循环引用单元测试:反射检测与映射配置验证咨询
关于AutoMapper循环引用检测与映射配置验证的问题
我们项目采用AutoMapper,近期引入了AutoMapper.Collection包用于N层架构中处理断开连接的数据库实体状态,目前整体运行正常,但遇到循环引用模型时(如下代码),若在CreateMap配置中未指定.MaxDepth()或.PreserveReferences(),进行双向映射会触发System.StackOverFlowException。
我们知晓Dto层的循环引用大多属于代码异味,但仍希望规避人为错误或异常场景,拟定两种应对思路:一是全局配置AutoMapper的MaxDepth;二是通过System.Reflection扫描程序集,找出所有存在循环引用的模型,编写单元测试验证其映射配置中是否指定了MaxDepth或PreserveReferences。现咨询以下两个问题:
- 如何通过System.Reflection以最优方式检测程序集中的循环引用模型?
- 后续如何验证这些模型的映射配置中是否包含MaxDepth/PreserveReferences?是否需要遍历所有已注册映射,与循环引用模型列表对比并检查配置项?
循环引用模型示例代码
public class Foo { public List<Bar> Bars { get; set; } } public class Bar { public Foo Foo { get; set; } } public class FooDto { public List<BarDto> Bars { get; set; } } public class BarDto { public FooDto Foo { get; set; } }
问题1:用System.Reflection检测程序集中的循环引用模型
核心思路是遍历程序集内的自定义类型,追踪类型间的属性引用关系,检测是否存在闭环,同时通过缓存优化检测效率:
- 过滤系统类型:跳过string、int等系统内置值类型/常用引用类型,避免无意义检测
- 递归追踪属性引用:处理普通引用类型和集合类型(提取集合的元素类型),记录访问路径
- 缓存检测结果:存储已检测过的类型,避免重复计算
示例实现代码
private static readonly HashSet<Type> _systemTypes = new HashSet<Type> { typeof(string), typeof(DateTime), typeof(int), typeof(long), typeof(bool) }; private static readonly Dictionary<Type, bool> _cycleCache = new Dictionary<Type, bool>(); public static bool HasCycle(Type type, HashSet<Type> visited = null) { // 跳过系统类型或已缓存结果 if (_systemTypes.Contains(type)) return false; if (_cycleCache.TryGetValue(type, out var hasCycle)) return hasCycle; visited ??= new HashSet<Type>(); // 路径中重复出现当前类型,说明存在循环 if (!visited.Add(type)) { _cycleCache[type] = true; return true; } // 遍历所有公共实例属性 foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { var propType = property.PropertyType; // 处理集合类型,提取泛型元素类型 if (propType.IsGenericType && typeof(IEnumerable<>).IsAssignableFrom(propType.GetGenericTypeDefinition())) { propType = propType.GetGenericArguments()[0]; } // 仅检测自定义引用类型 if (propType.IsClass && !_systemTypes.Contains(propType)) { if (HasCycle(propType, new HashSet<Type>(visited))) { _cycleCache[type] = true; return true; } } } _cycleCache[type] = false; return false; } // 扫描程序集获取所有循环引用类型 var targetAssembly = Assembly.GetExecutingAssembly(); var cyclicTypes = targetAssembly.GetTypes() .Where(t => t.IsClass && !t.IsAbstract && HasCycle(t)) .ToList();
问题2:验证循环引用模型的映射配置
需要遍历AutoMapper已注册的所有映射配置,结合循环引用类型列表进行校验:
- 获取所有注册映射:通过
IMapperConfigurationProvider的GetAllTypeMaps()方法拿到所有TypeMap实例 - 筛选关联循环类型的映射:判断映射的源类型或目标类型是否属于循环引用类型列表
- 检查配置项:验证映射是否设置了
MaxDepth或PreserveReferences
单元测试示例代码
[Test] public void CyclicTypeMappings_MustHaveMaxDepthOrPreserveReferences() { // 加载项目的AutoMapper配置 var mapperConfig = new MapperConfiguration(cfg => { cfg.AddProfile<YourBusinessProfile>(); // 若有全局MaxDepth配置,可在此设置:cfg.MaxDepth(3); }); // 获取问题1中检测到的循环引用类型 var cyclicTypes = GetCyclicTypes(); // 筛选出未满足配置要求的映射 var invalidMappings = mapperConfig.GetAllTypeMaps() .Where(tm => cyclicTypes.Contains(tm.SourceType) || cyclicTypes.Contains(tm.DestinationType)) .Where(tm => tm.MaxDepth == null && !tm.PreserveReferences) .ToList(); // 断言验证,输出不符合要求的映射信息 Assert.That(invalidMappings, Is.Empty, $"以下循环引用类型的映射未配置MaxDepth或PreserveReferences:{string.Join(", ", invalidMappings.Select(tm => $"{tm.SourceType.Name} -> {tm.DestinationType.Name}"))}"); }
注意事项
- 若配置了全局
MaxDepth,所有映射会继承该设置,此时单元测试可验证全局配置是否生效,或针对特殊映射的自定义配置 - 双向映射(如
Foo↔FooDto)需确保两个方向的映射都满足配置要求
内容的提问来源于stack exchange,提问作者DavisZ
相关产品推荐
相关产品推荐

