【问题标题】:Why does Automapper map a concrete collection to a new instance each time the source collection is referenced?为什么每次引用源集合时 Automapper 都会将具体集合映射到新实例?
【发布时间】:2016-08-16 22:45:49
【问题描述】:

我有一个源对象,其中包含对同一集合的 2 个引用。如果我将源类型映射到结构等效的目标类型,AutoMapper 将在目标实例中创建集合的两个实例。

class SourceThing
{
    public string Name { get; set; }
    public List<int> Numbers { get; set; }
    public List<int> MoreNumbers { get; set; }
}

class TargetThing
{
    public string Name { get; set; }
    public List<int> Numbers { get; set; }
    public List<int> MoreNumbers { get; set; }
}

如果我创建一个SourceThing,它有两个对同一个列表的引用,将它映射到一个TargetThing,结果是一个TargetThing,它有两个单独的集合实例。

public void MapObjectWithTwoReferencesToSameList()
{
    Mapper.CreateMap<SourceThing, TargetThing>();
    //Mapper.CreateMap<List<int>, List<int>>(); // passes when mapping here

    var source = new SourceThing() { Name = "source" };
    source.Numbers = new List<int>() { 1, 2, 3 };
    source.MoreNumbers = source.Numbers;
    Assert.AreSame(source.Numbers, source.MoreNumbers);

    var target = Mapper.Map<TargetThing>(source);
    Assert.IsNotNull(target.Numbers);
    Assert.AreSame(target.Numbers, target.MoreNumbers); // fails
}

这是否意味着 AutoMapper 中具体集合的默认映射行为?通过测试,我意识到如果我将List&lt;int&gt;映射到List&lt;int&gt;,我实现了我想要的行为,但我不明白为什么。如果 AutoMapper 跟踪引用并且不重新映射映射对象,它不会看到 source.MoreNumbers 指向与 source.Numbers 相同的列表,并相应地设置目标吗?

【问题讨论】:

  • 听起来您遇到了与 Stack Overflow 问题中描述的相同的问题:AutoMapper, how to keep references between mapped objects?。他们使用自定义 ITypeConverter 解决了这个问题。听起来这在您的场景中可能需要做很多工作(如果您必须为许多课程这样做)。
  • 它是相关的,但他的问题是关于跨 多个 映射操作保留引用,而我的问题是在单个 map() 中。这不是 AutoMapper 的意图,当每个 Mapper.Map() 调用仅依赖于静态映射配置时,怎么可能?

标签: c# .net automapper automapper-3


【解决方案1】:

我做了更多的研究和修补。在内部,当映射引擎遍历对象图时,它会为每个源类型/目标类型选择最佳映射器。除非存在非标准映射(过于简化),否则引擎接下来将为源和目标类型查找已注册的映射器。如果找到一个,它会创建目标对象,然后遍历并映射所有属性。它还将目标对象放入ResolutionContext.InstanceCache,即Dictionary&lt;ResolutionContext, object&gt;。如果在同一个根映射调用中再次遇到相同的源对象,它将从缓存中拉取对象,而不是浪费时间重新映射。

但是,如果没有注册的映射器,引擎会选择下一个适用的映射器,在这种情况下是AutoMapper.Mappers.CollectionMapper。集合映射器创建一个目标集合,枚举源集合并映射每个元素。它将目标对象添加到缓存中。这显然是设计。

解析上下文

我发现真正有趣的是对象是如何缓存在 InstanceCache 中的。键是当前的 ResolutionContext,它包含源和目标类型以及源值。 ResolutionContext 覆盖 GetHashCode() 和 Equals(),它们使用底层源值的相同方法。我可以在自定义类上定义相等性,以便具有该类的多个相等但不同的实例的源集合映射到具有对同一实例的多个引用的集合。

这个类:

class EquatableThing 
{
    public string Name { get; set; }

    public override bool Equals(object other)
    {
        if (ReferenceEquals(this, other)) return true;
        if (ReferenceEquals(null, other)) return false;

        return this.Name == ((EquatableThing)other).Name;
    }

    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }
}

用 2 个相等(但分开)的东西映射一个集合,结果是一个包含 2 个指向同一东西的指针的集合!

    public void MapCollectionWithTwoEqualItems()
    {
        Mapper.CreateMap<EquatableThing, EquatableThing>();

        var thing1 = new EquatableThing() { Name = "foo"};
        var thing2 = new EquatableThing() { Name = "foo"};

        Assert.AreEqual(thing1, thing2);
        Assert.AreEqual(thing1.GetHashCode(), thing2.GetHashCode());
        Assert.AreNotSame(thing1, thing2);

        // create list and map this thing across
        var list = new List<EquatableThing>() { thing1, thing2};
        var result = Mapper.Map<List<EquatableThing>, List<EquatableThing>>(list);
        Assert.AreSame(result[0], result[1]);
    }

保留引用

一方面,我想知道为什么 AutoMapper 的默认行为不会尽可能地将对象图映射到目标结构。 N 个源对象产生 N 个目标对象。但既然它没有,我很想看到 Map 方法上的一个选项来 PreserveReferences 就像序列化程序一样。如果选择了该选项,则映射的每个引用都使用引用相等比较器和源对象作为键并将目标作为值放置在字典中。本质上,如果某些东西已经被映射,则使用该映射的结果对象。

【讨论】:

    【解决方案2】:

    这种行为没有任何问题,这只是 automapper 映射的方式。

    在顶部,您创建了一个数字列表,然后将其应用于第二个数字列表。然后您可以比较它们是相同的,因为该对象有 2 个指向同一个列表的指针。它没有复制数字,它只是做了一个新的参考,就像你问的那样。

    现在,转到自动映射器。它贯穿并从一个对象映射到等效对象。它分别映射每个属性,复制信息。因此,即使源有更多数字作为指向同一个列表的指针,自动映射器也会单独映射每个。为什么?它是映射属性,而不是检查属性指针。而且,在大多数情况下,您不希望它这样做。

    这有意义吗?

    如果最终目标是通过测试,那么问题不是“做数字和更多数字指向同一个对象”,而是“做数字和更多数字包含完全相同的列表”。在第一种情况下,两者的答案都是肯定的,因为数字和更多数字都有一个对象(列表)。在第二种情况下,答案是假的,然后是真的,因为列表是等价的,但它并不指向同一个确切的对象。

    如果您真的希望它是同一个对象,您将不得不以不同的方式玩游戏。如果您只是想知道列表是否具有相同的元素,请更改断言。

    【讨论】:

    • 有意义吗?不是我。我小心翼翼地没有将这种行为描述为错误的。而不是 List,如果这是另一个自定义引用类型,并且两个属性引用同一个对象,AutoMapper 只会调用目标构造函数一次,它会生成与源匹配的目标对象。因此,虽然它只是映射属性,但它肯定是在查看引用。为了记录,我确实希望它这样做。如果我将 List 映射到 List,它会按照我期望的方式映射。
    • @drew 您可能希望它这样做,但一般来说,对象处理状态,而不是引用。当您查看一个对象时,重要的部分是每个属性都有一个可以检查的状态。因此,在映射时,您正在复制状态。它复制数字。它单独复制更多数字。现在,在某些情况下,它会检查引用,但一旦你嵌套很深就不会。你所拥有的是一个边缘案例。我很想看到为单个对象引用维护两个不同属性的商业理由,因为我不明白。你能解释一下吗?
    • 我的最终目标不是让考试通过。最终,我试图减少通过网络传输的冗余数据量。在序列化和通过网络发送之前,我们将一些复杂的 DTO 映射到扁平模型,我试图了解如何最好地利用 AutoMapper。如果一个对象包含数百个项目的集合,并且每个项目都引用相同的值类型集合,则 DTO 相当小。映射后,每个孩子的列表都是一个单独的实例,因此序列化的对象膨胀失控。
    • 哦,我现在明白了。这个例子有点过于简单了,所以我没有考虑更深层次的层次结构。一种选择是让您的 DTO 设置与客户端模型不同。从子对象中分离出列表,然后在子对象中包含映射 ID。我必须在提供整个目录的 Web 服务中执行此操作(不,我没有构建它,因为我认为 ReST 上的整个目录有点愚蠢)。如果可能的话,另一个方法是让原始调用提取摘要,然后在后续调用中仅填写层次结构所需的内容。这行得通吗?
    • 我正在与(反对?)一位过分热心的架构师合作,他不愿意更改共享模型,同样不愿意考虑任何无法完成的数据映射前/映射后安排自动映射器。我们已经使整个映射层 IMO 过于复杂,因此我们可以使用单线控制器方法。
    猜你喜欢
    • 2015-07-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-03-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多