17

This is a long one.

So, I have a model and a viewmodel that I'm updating from an AJAX request. Web API controller receives the viewmodel, which I then update the existing model using AutoMapper like below:

private User updateUser(UserViewModel entityVm)
{
    User existingEntity = db.Users.Find(entityVm.Id);
    db.Entry(existingEntity).Collection(x => x.UserPreferences).Load();

    Mapper.Map<UserViewModel, User>(entityVm, existingEntity);
    db.Entry(existingEntity).State = EntityState.Modified;

    try
    {
        db.SaveChanges();
    }
    catch
    { 
        throw new DbUpdateException(); 
    }

    return existingEntity;
}

I have automapper configured like so for the User -> UserViewModel (and back) mapping.

Mapper.CreateMap<User, UserViewModel>().ReverseMap();

(Note that explicitly setting the opposite map and omitting the ReverseMap exhibits the same behavior)

I'm having an issue with a member of the Model/ViewModel that is an ICollection of a different object:

[DataContract]
public class UserViewModel
{
    ...
    [DataMember]
    public virtual ICollection<UserPreferenceViewModel> UserPreferences { get; set; }
}

The corresponding model is like such:

public class User
{
    ...
    public virtual ICollection<UserPreference> UserPreferences { get; set; }
}

The Problem:

Every property of the User and UserViewModel classes maps correctly, except for the ICollections of UserPreferences/UserPreferenceViewModels shown above. When these collections map from the ViewModel to the Model, rather than map properties, a new instance of a UserPreference object is created from the ViewModel, rather than update the existing object with the ViewModel properties.

Model:

public class UserPreference
{
    [Key]
    public int Id { get; set; }

    public DateTime DateCreated { get; set; }

    [ForeignKey("CreatedBy")]
    public int? CreatedBy_Id { get; set; }

    public User CreatedBy { get; set; }

    [ForeignKey("User")]
    public int User_Id { get; set; }

    public User User { get; set; }

    [MaxLength(50)]
    public string Key { get; set; }

    public string Value { get; set; }
}

And the corresponding ViewModel

public class UserPreferenceViewModel
{
    [DataMember]
    public int Id { get; set; }

    [DataMember]
    [MaxLength(50)]
    public string Key { get; set; }

    [DataMember]
    public string Value { get; set; }
}

And automapper configuration:

Mapper.CreateMap<UserPreference, UserPreferenceViewModel>().ReverseMap();

//also tried explicitly stating map with ignore attributes like so(to no avail):

Mapper.CreateMap<UserPreferenceViewModel, UserPreference>().ForMember(dest => dest.DateCreated, opts => opts.Ignore());

When mapping a UserViewModel entity to a User, the ICollection of UserPreferenceViewModels is also mapped the User's ICollection of UserPreferences, as it should.

However, when this occurs, the individual UserPreference object's properties such as "DateCreated", "CreatedBy_Id", and "User_Id" get nulled as if a new object is created rather than the individual properties being copied.

This is further shown as evidence as when mapping a UserViewModel that has only 1 UserPreference object in the collection, when inspecting the DbContext, there are two local UserPreference objects after the map statement. One that appears to be a new object created from the ViewModel, and one that is the original from the existing model.

How can I make automapper update an existing Model's collection;s members, rather than instantiate new members from the ViewModel's collection? What am I doing wrong here?

Screenshots to demonstrate before/after Mapper.Map()

Before

After

Ron Brogan
  • 892
  • 1
  • 9
  • 25
  • You might need to override the `GetHashCode` in `UserPreference` and `UserPreferenceViewModel` so that the collection can identify them as being the same. Maybe automapper has some magic there but I am doubtful – HaroldHues Aug 20 '15 at 14:00
  • Yup, must have copy/pasted the wrong line for the CreateMap line. I'm looking into the GetHashCode override currently, I'll report back if anything is found. Thanks for your help. – Ron Brogan Aug 20 '15 at 14:15
  • 1
    You need to step back and stop using AutoMapper to map from ViewModel to your Entity or Domain Models. AutoMapper wasn't made for this and won't work as expected. Use AutoMapper **ONLY** to Map from Entity/Domain Model to ViewModel/DTO/BindingModel, **NEVER** other way around. Doing so is strongly discouraged by the AutoMapper author – Tseng Aug 20 '15 at 14:53
  • @Tseng, where can I read more about this? I haven't come across this distinction before. – Ron Brogan Aug 20 '15 at 15:01
  • https://lostechies.com/jimmybogard/2009/09/18/the-case-for-two-way-mapping-in-automapper/ (Jimmy is the Author of AutoMapper). Some other blog post explaining the issues with mapping to (rather than form) Domain Model / Entity: http://rogeralsing.com/2013/12/01/why-mapping-dtos-to-entities-using-automapper-and-entityframework-is-horrible/ – Tseng Aug 20 '15 at 17:59
  • 1
    Especially take a look at this post http://www.uglybugger.org/software/post/friends_dont_let_friends_use_automapper and comment from the AutoMapper Author Jimmy: "3) AutoMapper was never, ever intended to map *back* into a behavioral model. AutoMapper is intended to build DTOs, not map back in" – Tseng Aug 20 '15 at 17:59

3 Answers3

25

This is a limitation of AutoMapper as far as I'm aware. It's helpful to keep in mind that while the library is popularly used to map to/from view models and entities, it's a generic library for mapping any class to any other class, and as such, doesn't take into account all the eccentricities of an ORM like Entity Framework.

So, here's the explanation of what's happening. When you map a collection to another collection with AutoMapper, you are literally mapping the collection, not the values from the items in that collection to items in a similar collection. In retrospect, this makes sense because AutoMapper has no reliable and independent way to ascertain how it should line up one individual item in a collection to another: by id? which property is the id? maybe the names should match?

So, what's happening is that the original collection on your entity is entirely replaced with a brand new collection composed of brand new item instances. In many situations, this wouldn't be a problem, but when you combine that with the change tracking in Entity Framework, you've now signaled that the entire original collection should be removed and replaced with a brand new set of entities. Obviously, that's not what you want.

So, how to solve this? Well, unfortunately, it's a bit of a pain. The first step is to tell AutoMapper to ignore the collection completely when mapping:

Mapper.CreateMap<User, UserViewModel>();
Mapper.CreateMap<UserViewModel, User>()
    .ForMember(dest => dest.UserPreferences, opts => opts.Ignore());

Notice that I broke this up into two maps. You don't need to ignore the collection when mapping to your view model. That won't cause any problems because EF isn't tracking that. It only matters when you're mapping back to your entity class.

But, now you're not mapping that collection at all, so how do you get the values back on to the items? Unfortunately, it's a manual process:

foreach (var pref in model.UserPreferences)
{
    var existingPref = user.UserPreferences.SingleOrDefault(m => m.Id == pref.Id);
    if (existingPref == null) // new item
    {
        user.UserPreferences.Add(Mapper.Map<UserPreference>(pref));
    }
    else // existing item
    {
        Mapper.Map(pref, existingPref);
    }
}
Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • 1
    you're quickly becoming one of my fav contributors on here :) – Tim Tyler Mar 15 '18 at 16:48
  • 1
    do not forget to sync removed items: `user.UserPreferences.RemoveAll(x => model.UserPreferences.All(z => z.Id != x.Id));` – smg Nov 29 '18 at 17:05
9

In the meantime there exists an AutoMapper Extension for that particular problem:

cfg.AddCollectionMappers();
cfg.CreateMap<S, D>().EqualityComparison((s, d) => s.ID == d.ID);

With AutoMapper.EF6/EFCore you can also auto generate all equality comparisons. Plaese see AutoMapper.Collection AutoMapper.EF6 or AutoMapper.Collection.EFCore

r2d2
  • 592
  • 5
  • 15
  • 1
    Perfect! It's simple and works like a charm. This should be accepted as answer. Thank you. – zhe Jul 12 '19 at 11:49
2

According to the AutoMapper source file that handles all ICollection (among other things) and the ICollection Mapper:

The collection is cleared by a call to Clear() then added again, so as far as I can see there is no way that AutoMapper will be able to automagically do the mapping this time.

I would implement some logic to loop over the collections and AutoMapper.Map the ones that are the same

HaroldHues
  • 308
  • 1
  • 6