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?

2

There are 2 best solutions below

0
On BEST ANSWER

I did some more research and tinkering. Internally, as the mapping engine walks the object graph, it chooses the best mapper for each source type/destination type. Unless there is a non-standard mapping (oversimplified), the engine will next look for a registered mapper for source and destination type. If it finds one, it creates the destination object, then traverses and maps all of the properties. It also places that destination object into the ResolutionContext.InstanceCache, which is a Dictionary<ResolutionContext, object>. If the same source object is encountered again in the same root mapping call, it'll pull the object from the cache, instead of wasting time to re-map.

However, if there is no registered mapper, the engine chooses the next applicable mapper, which in this case is the AutoMapper.Mappers.CollectionMapper. The collection mapper creates a destination collection, enumerates the source collection and maps each element. It does not add the destination object into the cache. This is clearly the design.

Resolution Context

What I find really interesting is how objects are cached in the InstanceCache. The key is the current ResolutionContext, which contains the source and destination type and the source value. ResolutionContext overrides GetHashCode() and Equals(), which use the underlying source value's same methods. I can define equality on a custom class such that a source collection with multiple equal but distinct instances of that class maps to a collection with multiple references to the same instance.

This 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();
    }
}

Map a collection with 2 equal (but separate) things and the result is a collection with 2 pointers to the same thing!

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

Preserve References

I, for one, wonder why the default behavior of AutoMapper wouldn't be to map an object graph as closely as possible to the destination structure. N source objects results in N destination objects. But since it doesn't, I'd love to see an option on the Map method to PreserveReferences like a serializer would. If that option was picked, then every reference that is mapped is placed in a Dictionary using a reference equality comparer and source object for the key and the destination as the value. Essentially, if something is already mapped, the result object of that map is used.

6
On

There is nothing wrong with the behavior, it is just how automapper maps.

In the top section, you create a list of numbers and then apply it to a second list of numbers. You can then compare and they are the same because the object has 2 pointers to the same list. It did not copy the numbers, it simply made a new reference, just like you asked.

Now, move to the automapper. It runs through and maps from one object to an equivalent object. It maps each of the properties separately, copying the information. So, even though the source has morenumbers as a pointer to the same list, automapper maps each individually. Why? It is mapping properties, not examining the property pointers. And, you would not want it to do this in most instances.

Does this make sense?

If the ultimate goal is to get a test passing, the question is not "do numbers and more numbers point at the same object", but rather "do numbers and more numbers contain the exact same list". In the first instance, the answer to both is yes, as there is a single object (list) for numbers and more numbers. In the second, the answer is false, then true, as the list is equivalent, but it does not point to the same exact object.

If you truly want it to be the same object, you will have to play the game a bit differently. If you simply want to know if the list has the same elements, then change the assertion.