3

I have a Dto that looks somewhat like this:

class TypeDto
{    
    int Id {get; set;}
    string Name {get; set;}
    string DisplayName {get; set;}
    IEnumerable<TypeDto> Children {get; set;}
}

Now I need to map to it from two different sources. That's because one of them contains Name and the other contains DisplayName. So the types:

class Type1
{
    int Id {get; set;}
    string Name {get; set;}
    IEnumerable<Type1> Children {get; set;}
}

class Type2
{
    int Id {get; set;}
    string DisplayName {get; set;}
    IEnumerable<Type2> Nested {get; set;}
}

Notice the name difference in the Children/Nested enumerable.

Now for the map I would do:

config.CreateMap<Type1, TypeDto>();

config.CreateMap<Type2, TypeDto>()
    .ForMember(dest => dest.Children, opts => opts.MapFrom(src => src.Nested));

var dto = _mapper.Map<TypeDto>(type1Instance);

_mapper.Map(type2Instance, dto);

The first map works as expected, mapping the children recursively, filling the Id and Name fields and leaving DisplayName equal to null everywhere. The second map, however, fills in the DisplayName for the root object properly, but then in its children, it nullifies the Name field. So for example:

var type1Instance = new Type1 
{ 
    Id = 1, 
    Name = "root", 
    Children = new[] { new Type1 
        {
            Id = 2,
            Name = "child"
        }}
};

var type2Instance = new Type2 
{ 
    Id = 1, 
    DisplayName = "Root", 
    Children = new[] { new Type2
        {
            Id = 2,
            DisplayName = "Child"
        }}
};

After mapping the following instances the resultant has its fields set to:

Id = 1,
Name = "root",
DisplayName = "Root",
Children = { TypeDto { Id = 2, Name = null, DisplayName = "Child", Children = null } }

so the child's Name is nullified, and that's not what I want. I'd like it to be "child", obviously. How should I configure the mapper to get the wanted behavior?

I cannot change the Type1 or Type2 classes, they're from an external API.

AutoMapper's version is 6.2.1, .NET Framework 4.5.1.

V0ldek
  • 9,623
  • 1
  • 26
  • 57
  • Collections are cleared first, check AutoMapper.Collection. Check [the execution plan](http://docs.automapper.org/en/stable/Understanding-your-mapping.html). – Lucian Bargaoanu Jul 23 '18 at 10:42
  • AutoMapper.Collection solves the issue. Adding an `EqualityComparison` by `Id` between all the mappings causes AutoMapper to properly update the Collections. Can you make this comment into an answer so I can accept it? – V0ldek Jul 23 '18 at 11:02
  • Feel free to post the answer yourself. – Lucian Bargaoanu Jul 23 '18 at 12:22

2 Answers2

3

This is one possible solution:

config.CreateMap<Type2, TypeDto>()
        .ForMember(dest => dest.Children, opts => opts.Ignore())
        .AfterMap((d,e) => AddNestedChildren(d, e));

private void AddNestedChildren(Type2 type, TypeDto dto)
{
    foreach (var child in type.Nested)
    {
        var childDto = dto.Children.SingleOrDefault(c => c.Id == child.Id);
        // keep old properties
        Mapper.Map(child, childDto);
    }
}

As stated in the comments, collections are nullified by default. One solution I could find to your problem, is using the AfterMap in order to manually loop through the children/nested collection, find the corresponding child for the other collection (by the Id or any other property you find relevant), and manually map it.

I basically used the same idea from my answer to this question.

Alisson Reinaldo Silva
  • 10,009
  • 5
  • 65
  • 83
  • Doesn't work. Also I think AutoMapper does not touch properties that are not present in the source of the mapping (hence the root object has its `Name` filled). – V0ldek Jul 23 '18 at 11:00
  • @V0ldek now that you mentioned, actually AutoMapper should throw an exception because it requires you to map all properties in the destination type. See my edited answer, and let me know if that helps you. – Alisson Reinaldo Silva Jul 23 '18 at 11:32
  • The `AutoMapper.Collection` does kind of the same thing, so I went and used that (see "my" answer based on Lucian Bargaoanu's comment). Thanks for this solution though. – V0ldek Jul 23 '18 at 12:35
3

Taken from Lucian Bargaoanu's comment.

The AutoMapper.Collection package solves my problem. All that was needed is adding this statement to configuration:

config.AddCollectionMappers();

and then defining EqualityComparison on both my maps:

config.CreateMap<Type1, TypeDto>()
    .EqualityComparison((src, dest) => src.Id == dest.Id);

config.CreateMap<Type2, TypeDto>()
    .EqualityComparison((src, dest) => src.Id == dest.Id)
    .ForMember(dest => dest.Children, opts => opts.MapFrom(src => src.Nested));

After that the collections are properly updated. Citing the docs from here, the second mapping, i.e.

_mapper.Map(type2Instance, dto);

will now map recursively on any collection members with matching Id, add to the collection and map any items from type2Instance.Nested which do not occur in dto.Children collection and remove any items that dto.Children contains, but type2Instance.Nested does not.

V0ldek
  • 9,623
  • 1
  • 26
  • 57