【问题标题】:How to discover missing type-maps for mapping enum to enum in AutoMapper?如何在 AutoMapper 中发现将枚举映射到枚举的缺失类型映射?
【发布时间】:2021-01-24 22:26:18
【问题描述】:

好的,伙计们,这是一个相当长的问题,在我提出实际问题之前,我会尽力描述当前情况并提供一些有意义的背景信息。

TL;DR;

我需要一种方法来识别无效的枚举到枚举映射,这可能会导致运行时问题,因为它们的定义随着时间的推移而出现分歧。

一些上下文

所以我和我的团队正在维护这组相当复杂的 REST-API...至少在涉及到实际涉及的对象图时很复杂。 我们总共要处理数百个模型。 为了提高结构复杂性,原始架构在内部 API 级别上采用了完整的 n 层样式。

最重要的是,我们有多个这样的架构服务,有时需要相互调用。 这是通过这里的普通 http 调用,那里的一些消息传递来实现的,你明白了。

为了让一个 API 与另一个 API 进行通信,并维护 SOA 和/或微服务原则,每个 API 至少提供一个相应的客户端库,该库管理与其代表 API 的通信,而不管实际的底层协议如何参与。

归结起来,每个 API 至少包含以下层(自上而下)

  • 客户层
  • API 层
  • 领域层
  • 持久层

此外,所有这些层都维护自己对各种模型的表示。通常,这些是 1:1 表示,只是在另一个命名空间中。有时这些层之间存在更显着的差异。这取决于...

为了在这些层之间进行通信时减少样板,我们大部分时间都在使用 AutoMapper(讨厌或喜欢它)。

问题:

随着我们整个系统的发展,我们越来越注意到在模型的各种表示中映射枚举到枚举属性时出现的问题。 有时是因为一些开发人员只是忘记在其中一个层中添加一个新的枚举值,有时我们重新生成了一个基于 Open-API 的生成客户端等,然后导致这些定义不同步枚举。主要问题是,源枚举可能比目标枚举具有更多的值。 当命名略有不同时,可能会出现另一个问题,例如执行者与执行者

假设我们有这个(非常非常过度简化的)模型表示


    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 工作),在尝试缓解此类问题时,没有太多工作要做。

很高兴,有一个不错的选项可用,它利用 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(和每个层)可能要处理几十个这样的案例。

我们怎样才能做到这一点,我们已经验证并调整了我们现有的代码库,并从一开始就进一步防止这种愚蠢行为?

【问题讨论】:

    标签: c# enums automapper


    【解决方案1】:

    利用自定义验证在测试期间发现缺失的映射

    好的,现在这种方法利用了多阶段分析,最适合单元测试(尽管如此,它可能已经存在于您的解决方案中)。 它不是神奇地解决你所有可能普遍存在的问题的金枪,而是让你进入一个非常紧凑的开发循环,这应该有助于清理事情。 期间。

    涉及的步骤是

    1. 启用 AutoMapper 配置验证
    2. 使用 AutoMapper 自定义验证来发现缺失的类型映射
    3. 添加和配置缺少的类型映射
    4. 确保地图有效
    5. 调整枚举或映射逻辑的变化(最适合的)

      这可能很麻烦,需要额外注意,具体取决于此方法发现的问题

    6. 冲洗并重复

    以下示例使用 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();
                });
    

    这做了几件事:

    1. 寻找映射,将一个枚举一个枚举映射
    2. 没有与之关联的类型映射(也就是说,它们是由 AutoMapper 自身“生成”的,因此缺少明确的 CreateMap 调用)
        if (!context.Types.DestinationType.IsEnum) return;
        if (!context.Types.SourceType.IsEnum) return;
        if (context.TypeMap is not null) return;
    
    1. 引发错误,该消息相当于缺少对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 中使用。

    1. 调整枚举的变化

    从上面重新运行测试,现在也应该失败,因为存在不兼容的枚举值。 输出应如下所示:

        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. 直到所有问题都解决。

    从那时起,你应该有一个安全网,它应该可以防止你将来落入那种陷阱。

    但是,请注意,映射 per-name 而不是 per-value 可能 会对性能产生负面影响。在将这种更改应用于您的代码库时,应该考虑到这一点。但是由于存在所有这些层间映射,我猜想可能的瓶颈在另一座城堡,马里奥;)

    可以在github-repo 中找到本文中显示的示例的完整总结

    【讨论】:

    • 但是为什么你需要一张枚举地图呢?无论如何,您都可以进行验证。
    • 好点。在我们的场景中,可能需要在映射本身内进行更多自定义。因此,这种方法至少可以确保为每个需要的案例定义了地图。
    • 当然,我可以通过删除 EnumMapper 本身来获得类似的结果,但这只会告诉我,从 TSource 到 Tdestination 缺少一个映射。这个解决方案在这里吐出实际需要的代码行,这是首先定义映射所需的代码行。
    • 在我看来,您很少需要地图本身。所以一个映射器(也许是默认的)会覆盖大多数情况,然后你为特殊情况添加映射。这是我认为最好的方法。
    【解决方案2】:

    我编写了一个验证器来检查枚举是否匹配,因此我不必添加那些枚举映射。

    var problematicMapperConfiguration = new MapperConfiguration(config =>
    {
        config.Advanced.Validator(EnumMappingValidator.ValidateNamesMatch());
        config.CreateMap<SourceType, DestinationType>();
    });
    
    problematicMapperConfiguration.AssertConfigurationIsValid();
    

    用你的例子它会失败:

    Expected enum AutoMapperEnumValidation.EarlocTests+Destination to contain enum "D".
    Expected enum AutoMapperEnumValidation.EarlocTests+Destination to contain enum "Executer".
    Expected enum AutoMapperEnumValidation.EarlocTests+Destination to contain enum "A1".
    Expected enum AutoMapperEnumValidation.EarlocTests+Destination to contain enum "B2".
    Expected enum AutoMapperEnumValidation.EarlocTests+Destination to contain enum "C3".
    

    验证器非常简单,如下所示:

    public class EnumMappingValidator
    {
        public static Action<ValidationContext> ValidateNamesMatch()
        {
            return validationContext =>
            {
                var sourceEnumType = GetEnumType(validationContext.Types.SourceType);
    
                if (sourceEnumType == null)
                    return;
                
                var destinationEnumType = GetEnumType(validationContext.Types.DestinationType);
    
                if (destinationEnumType == null) throw new ArgumentException("Unexpected Enum to Non-Enum Map");
    
                var sourceEnumNames = sourceEnumType.GetFields().Select(x => x.Name).ToList();
                var destinationEnumNames = destinationEnumType.GetFields().Select(x => x.Name).ToList();
    
                var errors = new List<string>();
    
                foreach (var sourceEnumName in sourceEnumNames)
                {
                    if (destinationEnumNames.All(x => x.ToLower() != sourceEnumName.ToLower()))
                        errors.Add($"Expected enum {destinationEnumType} to contain enum \"{sourceEnumName}\".");
                }
    
                if (errors.Any())
                    throw new ArgumentException(string.Join(Environment.NewLine, errors));
            };
        }
    
        private static Type? GetEnumType(Type type)
        {
            if (type.IsEnum) return type;
    
            var nullableUnderlyingType = Nullable.GetUnderlyingType(type);
            if (nullableUnderlyingType?.IsEnum ?? false) return nullableUnderlyingType;
    
            return null;
        }
    }
    

    Github:https://github.com/matthiaslischka/AutoMapperEnumValidation

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2018-08-01
      • 1970-01-01
      • 2019-04-09
      • 1970-01-01
      • 2019-08-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多