1

我有一个源对象,其中包含对同一集合的 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对同一个列表有两个引用的 a ,将它映射到 a TargetThing,结果是 aTargetThing具有两个单独的集合实例。

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<int>List<int>,我实现了我想要的行为,但我不明白为什么。如果 AutoMapper 跟踪引用并且不重新映射映射对象,它不会看到source.MoreNumbers指向与 相同的列表source.Numbers,并相应地设置目标吗?

4

2 回答 2

4

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

但是,如果没有注册的映射器,引擎会选择下一个适用的映射器,在这种情况下是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 就像序列化程序一样。如果选择了该选项,则使用引用相等比较器和源对象作为键,将目标作为值,将映射的每个引用放置在字典中。本质上,如果某些东西已经被映射,则使用该映射的结果对象。

于 2015-08-14T16:49:29.807 回答
1

行为没有错,这只是自动映射器的映射方式。

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

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

这有意义吗?

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

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

于 2015-08-13T16:55:01.737 回答