Search code examples
c#.netautomapperautomapper-3

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


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?


Solution

  • 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.