Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Automapper creating new instance rather than map properties

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

like image 648
Ron Brogan Avatar asked Aug 20 '15 13:08

Ron Brogan


People also ask

Why you should not use AutoMapper?

1. If you use the convention-based mapping and a property is later renamed that becomes a runtime error and a common source of annoying bugs. 2. If you don't use convention-based mapping (ie you explicitly map each property) then you are just using automapper to do your projection, which is unnecessary complexity.

Is AutoMapper faster than manual mapping?

Inside this article, it discusses performance and it indicates that Automapper is 7 times slower than manual mapping.

Does AutoMapper map private properties?

AutoMapper will map property with private setter with no problem. If you want to force encapsulation, you need to use IgnoreAllPropertiesWithAnInaccessibleSetter. With this option, all private properties (and other inaccessible) will be ignored.


1 Answers

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);
    }
}
like image 158
Chris Pratt Avatar answered Oct 12 '22 01:10

Chris Pratt