为什么每次引用源集合时,Automapper 都会将一个具体集合映射到一个新实例?

Why does Automapper map a concrete collection to a new instance each time the source collection is referenced?

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

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

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

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

这有意义吗?

如果最终目标是通过考试,那么问题就不是 "do numbers and more numbers point at the same object",而是 "do numbers and more numbers contain the exact same list"。首先,两者的答案都是肯定的,因为只有一个对象(列表)用于数字和更多数字。在第二个中,答案为假,然后为真,因为列表是等价的,但它不指向同一个对象。

如果你真的想让它成为同一个物体,你就必须以不同的方式玩游戏。如果你只是想知道列表是否有相同的元素,那么改变断言。

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

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

解析上下文

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

这个class:

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。如果选择了该选项,则映射的每个引用都将放置在一个字典中,使用引用相等比较器和源对象作为键,目标作为值。本质上,如果某些东西已经映射,则使用该映射的结果对象。