1

I have a source object which contains 2 references to the same collection. If I map the source type to a structurally-equivalent target type, AutoMapper will create two instances of the collection in the target instance.

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

If I create a SourceThing which has two references to the same List, map it to a TargetThing, the result is a TargetThing with two separate instances of the collection.

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
}

Is this meant to be the default mapping behavior for concrete collections in AutoMapper? Through testing, I realized that if I mapped List<int> to List<int>, I achieve the behavior I want, but I don't understand why. If AutoMapper tracks references and doesn't re-map a mapped object, wouldn't it see that the source.MoreNumbers points to the same list as source.Numbers, and set the target accordingly?

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 個の宛先オブジェクトになります。しかし、そうではないので、シリアライザーのように PreserveReferences への Map メソッドのオプションを見てみたいです。そのオプションが選択された場合、マップされたすべての参照は、参照等価比較子とキーのソース オブジェクトと値としての宛先を使用してディクショナリに配置されます。基本的に、何かが既にマップされている場合、そのマップの結果オブジェクトが使用されます。

于 2015-08-14T16:49:29.807 に答える