5

I am trying to figure out how to merge two complex object instances using AutoMapper. The parent object has a property which is a collection of child objects:

public class Parent
{
    public List<Child> Children { get; set; }
}

public class Child
{
    public string A { get; set; }
    public string B { get; set; }
    public string C { get; set; }
}

I have two instances of Parent:

var parentOne = new Parent()
{
    Children = new List<Child>() { new Child() { A = "A", B = "B", C = "C" } }
};

var parentTwo = new Parent()
{
    Children = new List<Child>() { new Child() { C = "Updated value" } }
};

I would like to be able to merge values from parentOne to parentTwo, without overwriting the value of C in parentTwo. The maps I have created are as follows:

Mapper.CreateMap<Parent, Parent>()
    .ForMember(parent => parent.Children, opt => opt.UseDestinationValue());

Mapper.CreateMap<Child, Child>()
    .ForMember(child => child.C, opt => opt.Ignore());

Mapper.Map(parentOne, parentTwo);

As I understand it, AutoMapper will create new instances of complex properties unless you use the UseDestinationValue() option. However, after executing the code above, parentTwo.C equals "C" instead of "Updated value".

It looks to me like it's keeping the instance of List<Child>, but it is creating new instances of Child within the List. Unfortunately, I'm struggling to come up with a map that will keep each instance of Child as well.

Any help would be much appreciated!

Gareth James
  • 339
  • 2
  • 11

3 Answers3

6

As far as I know, AutoMapper doesn't support this and I believe it's because of the complexities of supporting such a scenario.

The UseDestinationValue does indeed only work on the collection instance as you surmised -- not the collection elements. In your scenario, there is only one item in each list and (I assume) you want child list objects updated "in sync" (i.e. first element updates first element, second updates second, etc...). But what if one list as 3 and the other list has 5? What does "UseDestinationValue" mean if there is no destination value? And how do you pick which desintation object to map among the two child lists? In a database scenario (which it sounds like was the basis for the question) there would be unique ids or some kind of foreign key which would allow you to match up the child objects to know which one to map to. In this case, it's too hard (IMO) to write a generic mapping that would support UseDestinationValue on individual elements of a collection.

PatrickSteele
  • 14,489
  • 2
  • 51
  • 54
  • Thanks Patrick - your're quite right of course. What I'm doing assumes the same elements are in each each list. As it happens this is the case for what I'm working on, but I suppose it's not really applicable for the majority of situations. I'll probably flesh out my code to keep collections in sync properly, and have AutoMapper merge properties for elements that match. – Gareth James Mar 20 '12 at 09:27
  • Thanks Patrick: I have a similar question [here](http://stackoverflow.com/questions/14229798/), and your comment "But what if one list has 3 and the other list has 5?" has illustrated that what I was trying to do probably didn't make sense. – Merenzo Jan 09 '13 at 09:19
4

It's looking more and more likely that I will be unable to come up with mappings to handle this scenario; fair enough really as AutoMapper was never intended to be used to merge values in this way.

So I have resorted to handling the collection manually within a loop. For the purposes of this answer, I am assuming that Parent.A is uniquely identifying property:

Mapper.CreateMap<Parent, Parent>()
    .ForMember(parent => parent.Children, opt => opt.Ignore());

Mapper.CreateMap<Child, Child>()
    .ForMember(child => child.C, opt => opt.Ignore());

Mapper.Map(parentOne, parentTwo);

foreach (var childTwo in parentTwo.Children)
{
    var childOne = parentOne.Children.Where(child => child.A == childTwo.A).Single();
    Mapper.Map(childOne, childTwo);
}
Gareth James
  • 339
  • 2
  • 11
  • 2
    Hi @Gareth - I implemented your solution in my app, with a slight tweak: after the Parent to Parent mapping, I called: `.AfterMap((src, dest) => { _Gareth's foreach loop_ });`. This allowed me to keep the loop with the mapping definition , rather than having to code it explictly after each call to `Map()`. – Merenzo Jan 09 '13 at 11:44
  • Gareth - you can also answer my similar question [here](http://stackoverflow.com/questions/14229798/) as your approach is probably the best fit for my situation. – Merenzo Jan 15 '13 at 04:13
2

I ran into this same problem when mapping from ViewModel (DTO) to EntitySet. Here's the method that I wrote to solve the issue. It synchronizes a collection of ViewModels into a collection of Entities.

In your automapper mapping you will need to ignore the collection entirely.

public void SyncronizeEntitySet<TViewModel, TEntity>(IEnumerable<TViewModel> modelSet, ICollection<TEntity> entitySet,
        Func<TViewModel, int> sourceKey, Func<TEntity, int> destinationKey, Action<TEntity> setParentKey)
    where TViewModel : class, new()
    where TEntity : class, new()
{
    var toDelete = new List<TEntity>();
    foreach (var entityItem in entitySet)
    {
        var modelItem = modelSet.FirstOrDefault(i => sourceKey(i) == destinationKey(entityItem));
        if (modelItem == null)
        {
            toDelete.Add(entityItem);
        }
        else
        {
            Mapper.Map(modelItem, entityItem);
        }
    }

    toDelete.ForEach(i => Delete(i));

    foreach (var modelItem in modelSet)
    {
        if (sourceKey(modelItem) == 0)
        {
            var entityItem = Mapper.Map<TEntity>(modelItem);
            setParentKey(entityItem);
            Add(entityItem);
        }
    }
}
Clint Simon
  • 971
  • 1
  • 6
  • 9