简体   繁体   English

如何在 AutoMapper 中发现将枚举映射到枚举的缺失类型映射?

[英]How to discover missing type-maps for mapping enum to enum in AutoMapper?

Ok folks, this is a rather long question, where I´m trying my best to describe the current situation and provide some meaningful context, before I´m comming to my actual question.好的,伙计们,这是一个相当长的问题,在我提出实际问题之前,我会尽力描述当前情况并提供一些有意义的背景信息。

TL;DR; TL;博士;

I need a way to identify invalid enum-to-enum mappings, which might cause runtime-issues, as their definitions diverged over the time.我需要一种方法来识别无效的枚举到枚举的映射,这可能会导致运行时问题,因为它们的定义随着时间的推移而出现分歧。

Some Context一些背景

So my team and I are maintaining this rather complex set of REST-APIs...complex at least when it comes down to the actual object-graphs involved.因此,我和我的团队正在维护这组相当复杂的 REST-API……至少在涉及到的实际对象图方面是复杂的。 We have to deal with some hundreds of models in total.我们总共要处理数百个模型。 To raise structural complexity, the original architectures went with full n-tier-style on an inner API-level.为了提高结构复杂性,原始架构在内部 API 级别上采用了完整的 n 层样式。

On top of that, we´re having multiple of such architectured services, which sometimes need calling each other.最重要的是,我们有多个这样的架构服务,有时需要相互调用。 This is achieved through either ordinary http-calls here, some messaging there, you get the idea.这是通过这里的普通 http 调用,那里的一些消息传递来实现的,你明白了。

For letting an API communicate with another, and to maintain SOA- and/or microservice-principles, every API at least provides a corresponding client-library, which manages communication with it´s representing API, regardless of the actual underlying protocol involved.为了让一个 API 与另一个通信,并维护 SOA 和/或微服务原则,每个 API 至少提供一个相应的客户端库,该客户端库管理与代表 ZDB9742387104CA8DE634A7CE1 的实际通信的通信。

Boiling this down, this incorporates at least the following layers per API (top-down)归结起来,每个 API 至少包含以下层(自上而下)

  • Client-Layer客户层
  • API-Layer API层
  • Domain-Layer领域层
  • Persistence-Layer持久层

Additionally, all those layers maintain their own representation of the various models.此外,所有这些层都维护自己对各种模型的表示。 Often, those are 1:1 representation, just in another namespace.通常,这些是 1:1 表示,只是在另一个命名空间中。 Sometimes there are more significant differences in between these layers.有时这些层之间存在更显着的差异。 It depends...这取决于...

To reduce boiler-plate when communicating between these layers, we´re falling back on AutoMapper most of the time (hate it or love it).为了在这些层之间进行通信时减少样板,我们大部分时间都在使用 AutoMapper(讨厌或喜欢它)。

The problem:问题:

As we evolve our overall system, we more and more noticed problems when mapping enum-to-enum properties within the various representations of the models.随着我们整个系统的发展,我们越来越注意到在模型的各种表示中映射枚举到枚举属性时出现的问题。 Sometimes it´s because some dev just forgot to add a new enum-value in one of the layers, sometimes we re-generated an Open-API based generated client, etc., which then leads to out-of-sync definitions of those enums.有时是因为一些开发人员只是忘记在其中一个层中添加一个新的枚举值,有时我们重新生成了一个基于 Open-API 的生成客户端等,然后导致这些定义不同步枚举。 The primary issue is, that a source enum may have more values then the target enum.主要问题是,源枚举可能比目标枚举具有更多的值。 Another issue might occur, when there are slight differences in the naming, eg Executer vs. Executor当命名略有不同时,可能会出现另一个问题,例如 Executer 与 Executor

Let´s say we have this (very very over-simplified) model-representations假设我们有这个(非常非常过度简化的)模型表示


    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; }
    }

Now let´s say our AutoMapper config looks something like this:现在假设我们的 AutoMapper 配置看起来像这样:


var problematicMapper = new MapperConfiguration(config =>
{
    config.CreateMap<SourceType, DestinationType>();
}).CreateMapper();


So mapping the following model is kind of a jeopardy, semantic-wise (or at least offers some very odd fun while debugging).所以映射下面的 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?
        */

bare with me, as some situations here are staged and possibly more extreme than found in reality.对我来说是裸露的,因为这里的某些情况是上演的,可能比现实中发现的更极端。 Just wanted to point out some weird behavior, even with AutoMapper trying to gracefully handle most cases, like the re-orderings or different casings.只是想指出一些奇怪的行为,即使 AutoMapper 试图优雅地处理大多数情况,比如重新排序或不同的外壳。 Currently, we are facing either more values in the source-enum, or slightly differences in naming / typos目前,我们在源枚举中面临更多的值,或者在命名/拼写错误方面略有不同

Fewer fun can be observed, when this ultimately causes some nasty production-bugs, which also may have more or less serious business-impact - especially when this kind of issue only happens during run-time, rather than test- and/or build-time.当这最终导致一些讨厌的生产错误时,可以观察到更少的乐趣,这也可能或多或少地对业务产生严重影响——尤其是当这种问题只发生在运行时,而不是测试和/或构建时时间。

Additionally, the problem is not exclusive to n-tier-ish architectures, but could also be an issue in orthogonal/onion-/clean-ish-architecture styles (wheras in such cases it should be more likely that such value-types would be placed somewhere in the center of the APIs, rather than on every corner / outer-ring /adapter-layer or whatever the current terminology is)此外,该问题不仅限于 n 层架构,还可能是正交/洋葱/clean-ish 架构 styles 中的问题(而在这种情况下,这种值类型应该更有可能是放置在 API 中心的某个位置,而不是每个角落/外环/适配器层或任何当前术语)

A (temporary) solution (临时)解决方案

Despite trying to reduce the shear amount of redundancy within the respective layers, or (manually) maintaining explicit enum-values within the definitions itself (which both are valid options, but heck, this is a lot of PITA-work), there is not much left to do while trying to mitigate this kind of issues.尽管试图减少各个层内的冗余剪切量,或者(手动)在定义本身内维护明确的枚举值(这两个都是有效的选项,但是见鬼,这是很多 PITA 工作),没有在尝试缓解此类问题时还有很多工作要做。

Gladly, there is a nice option available, which levereages mapping enum-to-enum-properties per-name instead of per-value , as well as doing more customization on a very fine-granular level on a per-member-basis.很高兴,有一个不错的选项可用,它利用 enum-to-enum-properties per-name而不是per-value映射,以及在每个成员的基础上在非常细粒度的级别上进行更多自定义。

[AutoMapper.Extensions.EnumMapping] to the rescue! [AutoMapper.Extensions.EnumMapping] 来救援!

from the docs:来自文档:

The package AutoMapper.Extensions.EnumMapping will map all values from Source type to Destination type if both enum types have the same value (or by name or by value)如果两个枚举类型具有相同的值(或按名称或按值),则 package AutoMapper.Extensions.EnumMapping 将 map 从源类型到目标类型的所有值

and

This package adds an extra EnumMapperConfigurationExpressionExtensions.EnableEnumMappingValidation extension method to extend the existing AssertConfigurationIsValid() method to validate also the enum mappings.这个 package 添加了一个额外的 EnumMapperConfigurationExpressionExtensions.EnableEnumMappingValidation 扩展方法来扩展现有的 AssertConfigurationIsValid() 方法来验证枚举映射。

To enable and cusomize mappings, one should just need to create the respective type-maps within AutoMapper-configuration:要启用和自定义映射,只需在 AutoMapper-configuration 中创建相应的类型映射:

var mapperConfig = new MapperConfiguration(config =>
{
    config.CreateMap<SourceType, DestinationType>();
    config.CreateMap<Source, Destination>().ConvertUsingEnumMapping(opt => opt.MapByName());
    config.EnableEnumMappingValidation();
});

mapperConfig.AssertConfigurationIsValid();

Which then would validate even enum-to-enum mappings.然后它将验证枚举到枚举的映射。

The question (finally ^^)问题(终于^^)

As our team previously did not (need to) configure AutoMapper with maps for every enum-to-enum mapping (as was the case for dynamic-maps in previous-versions of AutoMapper), we´re a bit lost on how to efficiently and deterministically discover every map needed to be configured this way.由于我们的团队以前没有(不需要)为每个枚举到枚举的映射配置 AutoMapper(就像 AutoMapper 以前版本中的动态映射一样),我们对如何有效地和确定性地发现需要以这种方式配置的每个 map。 Especially, as we´re dealing with possibly a couple of dozens of such cases per api (and per layer).特别是,因为我们每个 api(和每层)可能处理几十个这样的案例。

How could we possibly get to the point, where we have validated and adapted our existing code-base, as well as further preventing this kind of dumbery in the first place?我们怎么可能做到这一点,我们已经验证并调整了我们现有的代码库,并从一开始就进一步防止这种愚蠢行为?

Leverage custom validation to discover missing mappings during test-time利用自定义验证在测试期间发现缺失的映射

Ok, now this approach leverages a multi-phased analysis, best fitted into an unit-test (which may already be present in your solution(s), nevertheless).好的,现在这种方法利用了多阶段分析,最适合单元测试(尽管如此,它可能已经存在于您的解决方案中)。 It´s not a golden gun to magically solve all your issues which may be prevalent, but puts you into a very tight dev-loop which should help clean up things.它不是神奇地解决所有可能普遍存在的问题的金枪,而是让你进入一个非常紧凑的开发循环,这应该有助于清理问题。 Period.时期。

The steps involved are涉及的步骤是

  1. enable validation of your AutoMapper-configuration启用 AutoMapper 配置的验证
  2. use AutoMapper custom-validation to discover missing type maps使用 AutoMapper 自定义验证来发现缺失的类型映射
  3. add and configure missing type-maps添加和配置缺少的类型映射
  4. ensure maps are valid确保地图有效
  5. adapt changes in enums, or mapping logic (whatever best fits)适应枚举或映射逻辑的变化(最适合的)

    this can be cumbersome and needs extra attention, depending on the issues discovered by this approach这可能很麻烦,需要额外注意,具体取决于此方法发现的问题

  6. rinse and repeat冲洗并重复

Examples below use xUnit.下面的示例使用 xUnit。 Use whatever you might have at hands.使用您手头可能拥有的任何东西。

0. starting point 0.起点

We´re starting with your initial AutoMapper-configuration:我们从您的初始 AutoMapper 配置开始:

var mapperConfig = new MapperConfiguration(config =>
{
    config.CreateMap<SourceType, DestinationType>();
});

1. enable validation of your AutoMapper-Configuration 1. 启用 AutoMapper-Configuration 的验证

Somewhere within your test-suit, ensure you are validating your AutoMapper-configuration:在您的测试服中的某处,确保您正在验证您的 AutoMapper 配置:


[Fact]
public void MapperConfigurationIsValid() => mapperConfig.AssertConfigurationIsValid();

2. use AutoMapper custom-validation to discover missing type maps 2. 使用 AutoMapper 自定义验证来发现缺失的类型映射

Now modify your AutoMapper-configuration to this:现在将您的 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();
            });

This does a couple of things:这做了几件事:

  1. look for mappings, that map from an enum to an enum查找映射,即 map枚举枚举
  2. which have no type-map associated to them (that is, they were "generated" by AutoMapper itself and hence are lacking an explicit CreateMap call)没有与之关联的类型映射(也就是说,它们是由 AutoMapper 本身“生成”的,因此缺少显式的CreateMap调用)
    if (!context.Types.DestinationType.IsEnum) return;
    if (!context.Types.SourceType.IsEnum) return;
    if (context.TypeMap is not null) return;
  1. Raise an error, which message is the equivalent of the actual call missing to CreateMap引发错误,该消息相当于缺少对CreateMap的实际调用
var message = $"config.CreateMap<{context.Types.SourceType}, {context.Types.DestinationType}>().ConvertUsingEnumMapping(opt => opt.MapByName());";

throw new AutoMapperConfigurationException(message);

3. add and configure missing type-maps 3. 添加和配置缺少的类型映射

Re-running our previous test, which should fail now, should output something like this:如果 output 像这样,重新运行我们之前应该失败的测试:

AutoMapper.AutoMapperConfigurationException : config.CreateMap<Sample.AutoMapper.EnumValidation.Source, Sample.AutoMapper.EnumValidation.Destination>().ConvertUsingEnumMapping(opt => opt.MapByName());

And boom, there you go.和繁荣,你有go。 The missing type-map configuration call on a silver-plate.银盘上缺少的类型映射配置调用。

Now copy that line and place it somewhere suitable withing your AutoMapper-configuration.现在复制该行并将其放置在适合您的 AutoMapper 配置的地方。

For this post I´m just putting it below the existing one:对于这篇文章,我只是把它放在现有的下面:


config.CreateMap<SourceType, DestinationType>();
config.CreateMap<Sample.AutoMapper.EnumValidation.Source, Sample.AutoMapper.EnumValidation.Destination>().ConvertUsingEnumMapping(opt => opt.MapByName());

in a real-world scenario, this would be a line for every enum-to-enum mapping that not already has a type-map associated to it within the AutoMapper-configuration.在现实世界的场景中,这将是每个枚举到枚举映射的一行,这些映射在 AutoMapper 配置中还没有与之关联的类型映射。 Depending on how you actually configure AutoMapper, this line could need to be slightly adopted to your needs, eg for usage in MappingProfiles.根据您实际配置 AutoMapper 的方式,可能需要稍微采用此行以满足您的需要,例如用于 MappingProfiles。

  1. adapt changes in enums适应枚举的变化

Re-run the test from above, which should fail now, too, as there are incompatible enum-values.从上面重新运行测试,现在也应该失败,因为存在不兼容的枚举值。 The output should look something like this: 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

There you go, AutoMapper discovered missing or un-mappable enum-values. go,AutoMapper 发现缺少或不可映射的枚举值。

note that we lost automatic handling of differences in casing.请注意,我们失去了对大小写差异的自动处理。

What´s to do now heavily depends on your solution and cannot be covered in a SO-post.现在要做什么在很大程度上取决于您的解决方案,并且不能在 SO-post 中涵盖。 So take appropriate actions to mitigate.所以采取适当的措施来缓解。

6. rinse and repeat 6.冲洗并重复

Go back to 3. until all issues are solved. Go 回到 3. 直到所有问题都解决。

From then on, you should have a saftey-net in place, that should prevent you from falling into that kind of trap in the future.从那时起,你应该有一个安全网,这应该可以防止你将来落入那种陷阱。

However, note that mapping per-name instead of per-value might have a negative impact, performance-wise.但是,请注意,映射每个名称而不是每个值可能会对性能产生负面影响。 That should definetley be taken into account when applying this kind of change to your code-base.在将这种更改应用于您的代码库时,应该考虑到这一点。 But with all those inter-layer-mappings present I would guess a possible bottleneck is in another castle, Mario;)但是由于存在所有这些层间映射,我猜想可能的瓶颈在另一座城堡,马里奥;)

A full wrapup of the samples shown in this post can be found in this github-repo这篇文章中显示的示例的完整总结可以在这个github-repo中找到

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM