c# - 如何在 AutoMapper 中发现将枚举映射到枚举的缺失类型映射?
问题描述
好的,伙计们,这是一个相当长的问题,在我提出实际问题之前,我会尽力描述当前情况并提供一些有意义的背景信息。
TL;博士;
我需要一种方法来识别无效的枚举到枚举的映射,这可能会导致运行时问题,因为它们的定义随着时间的推移而出现分歧。
一些背景
因此,我和我的团队正在维护这组相当复杂的 REST-API……至少在涉及到的实际对象图方面是复杂的。我们总共要处理数百个模型。为了提高结构复杂性,原始架构在内部 API 级别上采用了完整的 n 层样式。
最重要的是,我们有多个这样的架构服务,有时需要相互调用。这是通过这里的普通 http 调用,那里的一些消息传递来实现的,你明白了。
为了让一个 API 与另一个 API 通信,并维护 SOA 和/或微服务原则,每个 API 至少提供一个相应的客户端库,该库管理与其代表 API 的通信,而不管所涉及的实际底层协议如何。
归结起来,每个 API 至少包含以下层(自上而下)
- 客户层
- 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();
因此,映射以下模型在语义方面是一种危险(或者至少在调试时提供了一些非常奇怪的乐趣)。
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 层架构,还可能是正交/洋葱/干净架构风格的问题(而在这种情况下,这种值类型应该更有可能是放置在 API 中心的某个位置,而不是每个角落/外环/适配器层或任何当前术语)
(临时)解决方案
尽管试图减少各个层内的冗余剪切量,或者(手动)在定义本身内维护明确的枚举值(这两个都是有效的选项,但是见鬼,这是很多 PITA 工作),没有在尝试缓解此类问题时还有很多工作要做。
很高兴,有一个不错的选项可用,它利用 enum-to-enum-properties per-name而不是per-value映射,以及在每个成员的基础上在非常细粒度的级别上进行更多自定义。
[AutoMapper.Extensions.EnumMapping] 来救援!
来自文档:
如果两个枚举类型具有相同的值(或按名称或按值),包 AutoMapper.Extensions.EnumMapping 会将所有值从 Source 类型映射到 Destination 类型
和
这个包添加了一个额外的 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 以前版本中的动态映射一样),我们对如何有效地和确定性地发现需要以这种方式配置的每个地图。特别是,因为我们每个 api(和每层)可能要处理几十个这样的案例。
我们怎么可能做到这一点,我们已经验证并调整了我们现有的代码库,并从一开始就进一步防止这种愚蠢行为?
解决方案
利用自定义验证在测试期间发现缺失的映射
好的,现在这种方法利用了多阶段分析,最适合单元测试(尽管如此,它可能已经存在于您的解决方案中)。它不是神奇地解决所有可能普遍存在的问题的金枪,而是让你进入一个非常紧凑的开发循环,这应该有助于清理问题。时期。
涉及的步骤是
- 启用 AutoMapper 配置的验证
- 使用 AutoMapper 自定义验证来发现缺失的类型映射
- 添加和配置缺少的类型映射
- 确保地图有效
- 适应枚举或映射逻辑的变化(最适合的)
这可能很麻烦,需要额外注意,具体取决于此方法发现的问题
- 冲洗并重复
下面的示例使用 xUnit。使用您手头可能拥有的任何东西。
0.起点
我们从您的初始 AutoMapper 配置开始:
var mapperConfig = new MapperConfiguration(config =>
{
config.CreateMap<SourceType, DestinationType>();
});
1. 启用 AutoMapper-Configuration 的验证
在您的测试服中的某处,确保您正在验证您的 AutoMapper 配置:
[Fact]
public void MapperConfigurationIsValid() => mapperConfig.AssertConfigurationIsValid();
2. 使用 AutoMapper 自定义验证来发现缺失的类型映射
现在将您的 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();
});
这做了几件事:
- 查找映射,即从枚举映射到枚举
- 没有与之关联的类型映射(也就是说,它们是由 AutoMapper 本身“生成”的,因此缺少显式
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);
3. 添加和配置缺少的类型映射
重新运行我们之前的测试,现在应该失败,应该输出如下内容:
AutoMapper.AutoMapperConfigurationException : config.CreateMap<Sample.AutoMapper.EnumValidation.Source, Sample.AutoMapper.EnumValidation.Destination>().ConvertUsingEnumMapping(opt => opt.MapByName());
和繁荣,你去。银盘上缺少的类型映射配置调用。
现在复制该行并将其放置在适合您的 AutoMapper 配置的地方。
对于这篇文章,我只是把它放在现有的下面:
config.CreateMap<SourceType, DestinationType>();
config.CreateMap<Sample.AutoMapper.EnumValidation.Source, Sample.AutoMapper.EnumValidation.Destination>().ConvertUsingEnumMapping(opt => opt.MapByName());
在现实世界的场景中,这将是每个枚举到枚举映射的一行,这些映射在 AutoMapper 配置中还没有与之关联的类型映射。根据您实际配置 AutoMapper 的方式,可能需要稍微采用此行以满足您的需要,例如用于 MappingProfiles。
- 适应枚举的变化
从上面重新运行测试,现在也应该失败,因为存在不兼容的枚举值。输出应如下所示:
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
你去了,AutoMapper 发现了缺失或不可映射的枚举值。
请注意,我们失去了对大小写差异的自动处理。
现在要做什么在很大程度上取决于您的解决方案,并且不能在 SO-post 中涵盖。所以采取适当的措施来缓解。
6.冲洗并重复
回到 3. 直到所有问题都解决。
从那时起,你应该有一个安全网,这应该可以防止你将来落入那种陷阱。
但是,请注意,映射每个名称而不是每个值 可能会对性能产生负面影响。在将这种更改应用于您的代码库时,应该考虑到这一点。但是,由于存在所有这些层间映射,我猜想可能的瓶颈在另一座城堡,马里奥;)
这篇文章中显示的示例的完整总结可以在这个github-repo中找到
推荐阅读
- aws-lambda - AWS ALB 不解码二进制响应
- cypress - 是否可以使用 apache2 服务器在 vps 上部署测试 cypress?
- java - 事件实体上的 Keycloak 覆盖 IP 地址
- reactjs - 如何在反应中显示当前的url路径?
- wordpress - 在 kubernates 上部署 bitnami/wordpress 的问题
- flutter - Admob 可以在没有 Firebase 的情况下运行吗?
- android - 关于 createUserWithEmailAndPassword 的问题
- node.js - 如何在 NodeJS 中使用 Graphql Apollo 和 SharpJS 处理上传的图像?
- python-3.x - 如何在 numba 中创建 datetime64[D]
- android - 如何在 Android SDK 30 中删除 wifi?