[英]How to discover missing type-maps for mapping enum to enum in AutoMapper?
好的,伙计们,这是一个相当长的问题,在我提出实际问题之前,我会尽力描述当前情况并提供一些有意义的背景信息。
我需要一种方法来识别无效的枚举到枚举的映射,这可能会导致运行时问题,因为它们的定义随着时间的推移而出现分歧。
因此,我和我的团队正在维护这组相当复杂的 REST-API……至少在涉及到的实际对象图方面是复杂的。 我们总共要处理数百个模型。 为了提高结构复杂性,原始架构在内部 API 级别上采用了完整的 n 层样式。
最重要的是,我们有多个这样的架构服务,有时需要相互调用。 这是通过这里的普通 http 调用,那里的一些消息传递来实现的,你明白了。
为了让一个 API 与另一个通信,并维护 SOA 和/或微服务原则,每个 API 至少提供一个相应的客户端库,该客户端库管理与代表 ZDB9742387104CA8DE634A7CE1 的实际通信的通信。
归结起来,每个 API 至少包含以下层(自上而下)
此外,所有这些层都维护自己对各种模型的表示。 通常,这些是 1:1 表示,只是在另一个命名空间中。 有时这些层之间存在更显着的差异。 这取决于...
为了在这些层之间进行通信时减少样板,我们大部分时间都在使用 AutoMapper(讨厌或喜欢它)。
随着我们整个系统的发展,我们越来越注意到在模型的各种表示中映射枚举到枚举属性时出现的问题。 有时是因为一些开发人员只是忘记在其中一个层中添加一个新的枚举值,有时我们重新生成了一个基于 Open-API 的生成客户端等,然后导致这些定义不同步枚举。 主要问题是,源枚举可能比目标枚举具有更多的值。 当命名略有不同时,可能会出现另一个问题,例如 Executer 与 Executor
假设我们有这个(非常非常过度简化的)模型表示
public enum Source { A, B, C, D, Executer, A1, B2, C3 } // more values than below
public enum Destination { C, B, X, Y, A, Executor } //fewer values, different ordering, no D, but X, Y and a Typo
class SourceType
{
public Source[] Enums { get; set; }
}
class DestinationType
{
public Destination[] Enums { get; set; }
}
现在假设我们的 AutoMapper 配置看起来像这样:
var problematicMapper = new MapperConfiguration(config =>
{
config.CreateMap<SourceType, DestinationType>();
}).CreateMapper();
所以映射下面的 model 在语义上是一种危险(或者至少在调试时提供了一些非常奇怪的乐趣)。
var destination = problematicMapper.Map<DestinationType>(new SourceType()
{
Enums = new []
{
Source.A,
Source.B,
Source.C,
Source.D,
Source.Executer,
Source.A1,
Source.B2,
Source.C3
}
});
var mappedValues = destination.Enums.Select(x => x.ToString()).ToArray();
testOutput.WriteLine(string.Join(Environment.NewLine, mappedValues));
/*
Source.A => A <- ✔️ ok
Source.B => b <- ✔️ok
Source.C => c <- ✔️ok
Source.D => Y <- 🤷♀️ whoops
Source.Executer => A <- 🧏♂️ wait, what?
Source.A1 => Executor <- 🙊 nah
Source.B2 => 6 <- 🙉 wtf?
Source.C3 => 7 <- 🙈 wth?
*/
对我来说是裸露的,因为这里的某些情况是上演的,可能比现实中发现的更极端。 只是想指出一些奇怪的行为,即使 AutoMapper 试图优雅地处理大多数情况,比如重新排序或不同的外壳。 目前,我们在源枚举中面临更多的值,或者在命名/拼写错误方面略有不同
当这最终导致一些讨厌的生产错误时,可以观察到更少的乐趣,这也可能或多或少地对业务产生严重影响——尤其是当这种问题只发生在运行时,而不是测试和/或构建时时间。
此外,该问题不仅限于 n 层架构,还可能是正交/洋葱/clean-ish 架构 styles 中的问题(而在这种情况下,这种值类型应该更有可能是放置在 API 中心的某个位置,而不是每个角落/外环/适配器层或任何当前术语)
尽管试图减少各个层内的冗余剪切量,或者(手动)在定义本身内维护明确的枚举值(这两个都是有效的选项,但是见鬼,这是很多 PITA 工作),没有在尝试缓解此类问题时还有很多工作要做。
很高兴,有一个不错的选项可用,它利用 enum-to-enum-properties per-name而不是per-value映射,以及在每个成员的基础上在非常细粒度的级别上进行更多自定义。
来自文档:
如果两个枚举类型具有相同的值(或按名称或按值),则 package AutoMapper.Extensions.EnumMapping 将 map 从源类型到目标类型的所有值
和
这个 package 添加了一个额外的 EnumMapperConfigurationExpressionExtensions.EnableEnumMappingValidation 扩展方法来扩展现有的 AssertConfigurationIsValid() 方法来验证枚举映射。
要启用和自定义映射,只需在 AutoMapper-configuration 中创建相应的类型映射:
var mapperConfig = new MapperConfiguration(config =>
{
config.CreateMap<SourceType, DestinationType>();
config.CreateMap<Source, Destination>().ConvertUsingEnumMapping(opt => opt.MapByName());
config.EnableEnumMappingValidation();
});
mapperConfig.AssertConfigurationIsValid();
然后它将验证枚举到枚举的映射。
由于我们的团队以前没有(不需要)为每个枚举到枚举的映射配置 AutoMapper(就像 AutoMapper 以前版本中的动态映射一样),我们对如何有效地和确定性地发现需要以这种方式配置的每个 map。 特别是,因为我们每个 api(和每层)可能处理几十个这样的案例。
我们怎么可能做到这一点,我们已经验证并调整了我们现有的代码库,并从一开始就进一步防止这种愚蠢行为?
好的,现在这种方法利用了多阶段分析,最适合单元测试(尽管如此,它可能已经存在于您的解决方案中)。 它不是神奇地解决所有可能普遍存在的问题的金枪,而是让你进入一个非常紧凑的开发循环,这应该有助于清理问题。 时期。
涉及的步骤是
这可能很麻烦,需要额外注意,具体取决于此方法发现的问题
下面的示例使用 xUnit。 使用您手头可能拥有的任何东西。
我们从您的初始 AutoMapper 配置开始:
var mapperConfig = new MapperConfiguration(config =>
{
config.CreateMap<SourceType, DestinationType>();
});
在您的测试服中的某处,确保您正在验证您的 AutoMapper 配置:
[Fact]
public void MapperConfigurationIsValid() => mapperConfig.AssertConfigurationIsValid();
现在将您的 AutoMapper 配置修改为:
mapperConfig = new MapperConfiguration(config =>
{
config.CreateMap<SourceType, DestinationType>();
config.Advanced.Validator(context => {
if (!context.Types.DestinationType.IsEnum) return;
if (!context.Types.SourceType.IsEnum) return;
if (context.TypeMap is not null) return;
var message = $"config.CreateMap<{context.Types.SourceType}, {context.Types.DestinationType}>().ConvertUsingEnumMapping(opt => opt.MapByName());";
throw new AutoMapperConfigurationException(message);
});
config.EnableEnumMappingValidation();
});
这做了几件事:
CreateMap
调用) if (!context.Types.DestinationType.IsEnum) return;
if (!context.Types.SourceType.IsEnum) return;
if (context.TypeMap is not null) return;
CreateMap
的实际调用var message = $"config.CreateMap<{context.Types.SourceType}, {context.Types.DestinationType}>().ConvertUsingEnumMapping(opt => opt.MapByName());";
throw new AutoMapperConfigurationException(message);
如果 output 像这样,重新运行我们之前应该失败的测试:
AutoMapper.AutoMapperConfigurationException : config.CreateMap<Sample.AutoMapper.EnumValidation.Source, Sample.AutoMapper.EnumValidation.Destination>().ConvertUsingEnumMapping(opt => opt.MapByName());
和繁荣,你有go。 银盘上缺少的类型映射配置调用。
现在复制该行并将其放置在适合您的 AutoMapper 配置的地方。
对于这篇文章,我只是把它放在现有的下面:
config.CreateMap<SourceType, DestinationType>();
config.CreateMap<Sample.AutoMapper.EnumValidation.Source, Sample.AutoMapper.EnumValidation.Destination>().ConvertUsingEnumMapping(opt => opt.MapByName());
在现实世界的场景中,这将是每个枚举到枚举映射的一行,这些映射在 AutoMapper 配置中还没有与之关联的类型映射。 根据您实际配置 AutoMapper 的方式,可能需要稍微采用此行以满足您的需要,例如用于 MappingProfiles。
从上面重新运行测试,现在也应该失败,因为存在不兼容的枚举值。 output 应如下所示:
AutoMapper.AutoMapperConfigurationException : Missing enum mapping from Sample.AutoMapper.EnumValidation.Source to Sample.AutoMapper.EnumValidation.Destination based on Name
The following source values are not mapped:
- B
- C
- D
- Executer
- A1
- B2
- C3
go,AutoMapper 发现缺少或不可映射的枚举值。
请注意,我们失去了对大小写差异的自动处理。
现在要做什么在很大程度上取决于您的解决方案,并且不能在 SO-post 中涵盖。 所以采取适当的措施来缓解。
Go 回到 3. 直到所有问题都解决。
从那时起,你应该有一个安全网,这应该可以防止你将来落入那种陷阱。
但是,请注意,映射每个名称而不是每个值可能会对性能产生负面影响。 在将这种更改应用于您的代码库时,应该考虑到这一点。 但是由于存在所有这些层间映射,我猜想可能的瓶颈在另一座城堡,马里奥;)
这篇文章中显示的示例的完整总结可以在这个github-repo中找到
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.