Automapper creating a new instance, not map properties

It is long.

So, I have a model and a view model that I am updating from an AJAX request. The Web API controller receives a view model, which then updates the existing model using AutoMapper, as shown 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 an automapper configured this way to display User -> UserViewModel (and vice versa).

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

(Note that explicitly setting the opposite display and throwing a ReverseMap exhibits the same behavior)

I am having a problem with a Model / ViewModel member, which is an ICollection of another object:

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

The corresponding model is as follows:

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

Problem:

Each property of the User and UserViewModel classes correctly displays, with the exception of ICollections UserPreferences / UserPreferenceViewModels, shown above. When these collections are displayed from the ViewModel in the Model, and not from the map properties, a new instance of the UserPreference object is created from the ViewModel, rather than updating the existing object using 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 the 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 to a user, ICollection of UserPreferenceViewModels also displays the user ICollection UserPreferences, as you would expect.

However, when this happens, individual properties of the UserPreference object, such as "DateCreated", "CreatedBy_Id", and "User_Id", are reset to zero, as if creating a new object, not individual copies that are being copied.

This is also shown as evidence that when matching a UserViewModel that has only 1 UserPreference object in the collection, when checking the DbContext after the map operator, there are two local UserPreference objects. One that looks like a new object created using the ViewModel, and one that is the original of the existing model.

How can I make automapper update existing elements of the Model collection rather than instantiate new members from the ViewModel collection? What am I doing wrong here?

Screenshots for demonstration before / after Mapper.Map ()

Before

After

+11
c # mapping automapper
source share
3 answers

This is a limitation of AutoMapper, as far as I know. It is useful to keep in mind that although the library is widely used to map to / from models and view entities, it is a common library for mapping any class to any other class and, as such, does not take into account all ORM eccentricities, for example, Entity Framework.

So, here is an explanation of what is happening. When you map a collection to another collection using AutoMapper, you literally map the collection, not the values ​​of the elements from this collection to the elements in a similar collection. In retrospect, this makes sense because AutoMapper does not have a reliable and independent way to determine how it should build one separate item in the collection to another: by id? which property is an identifier? perhaps the names should match?

So what happens is that the original collection on your entity is completely replaced by a completely new collection consisting of new instances of the elements. In many situations, this will not be a problem, but when you combine this with change tracking in the Entity Framework, you will now report that the entire original collection should be deleted and replaced with a new set of objects. Obviously, this is not what you want.

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

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

Please note that I split this into two cards. You do not need to ignore the collection when matching with your view model. This will not cause any problems because EF does not track it. This only matters when you return to the entity class.

But now you are not collecting this collection at all, since you are returning values ​​to elements? Unfortunately, this is 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); } } 
+16
source share

According to the original AutoMapper file , which handles all ICollection (among other things) and ICollection Mapper :

The collection is cleared by calling Clear() , and then added again, since I see that AutoMapper will not be able to automatically match this time.

I would follow some logic to AutoMapper.Map over collections and AutoMapper.Map those that are the same

+2
source share

At the same time, there is an AutoMapper extension for this particular problem:

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

With AutoMapper.EF6 / EFCore, you can also automatically generate all comparisons for equality. Please see AutoMapper.Collection AutoMapper.EF6 or AutoMapper.Collection.EFCore

0
source share

All Articles