Automapper, создающий новый экземпляр, а не свойства карты

Это длинный.

Итак, у меня есть модель и модель просмотра, которую я обновляю из запроса AJAX. Контроллер Web API получает модель просмотра, которая затем обновляет существующую модель с помощью AutoMapper, как показано ниже:

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;
}

У меня есть automapper, настроенный так для отображения User → UserViewModel (и обратно).

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

(Обратите внимание, что явно задание противоположного отображения и исключение ReverseMap проявляет то же поведение)

У меня возникла проблема с членом Model/ViewModel, который является ICollection другого объекта:

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

Соответствующая модель такова:

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

Проблема:

Каждое свойство классов User и UserViewModel корректно отображает, за исключением ICollections UserPreferences/UserPreferenceViewModels, показанного выше. Когда эти коллекции отображаются из ViewModel в Model, а не из свойств карты, новый экземпляр объекта UserPreference создается из ViewModel, а не обновляет существующий объект с помощью свойств ViewModel.

Модель:

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; }
}

И соответствующий ViewModel

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

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

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

И конфигурация automapper:

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());

При сопоставлении объекта UserViewModel с пользователем ICollection of UserPreferenceViewModels также отображает пользовательский ICollection UserPreferences, как и следовало ожидать.

Однако, когда это происходит, отдельные свойства объекта UserPreference, такие как "DateCreated", "CreatedBy_Id" и "User_Id", обнуляются, как будто создается новый объект, а не отдельные копии, которые копируются.

Это также показано в качестве доказательства того, что при сопоставлении UserViewModel, который имеет только 1 объект UserPreference в коллекции, при проверке DbContext после оператора карты есть два локальных объекта UserPreference. Тот, который выглядит как новый объект, созданный с помощью ViewModel, и тот, который является оригиналом существующей модели.

Как я могу заставить automapper обновлять существующие элементы коллекции Model, а не создавать экземпляры новых членов из коллекции ViewModel? Что я здесь делаю неправильно?

Скриншоты для демонстрации до/после Mapper.Map()

До

После

Ответ 1

Это ограничение AutoMapper, насколько мне известно. Полезно иметь в виду, что, хотя библиотека широко используется для отображения в/из моделей и сущностей представления, она является общей библиотекой для отображения любого класса в любой другой класс и, как таковая, не учитывает все эксцентриситеты ORM, например, Entity Framework.

Итак, вот объяснение того, что происходит. Когда вы сопоставляете коллекцию с другой коллекцией с помощью AutoMapper, вы буквально сопоставляете коллекцию, а не значения элементов из этой коллекции с элементами в подобной коллекции. В ретроспективе это имеет смысл, потому что AutoMapper не имеет надежного и независимого способа определить, как он должен выстроить один отдельный элемент в коллекции другому: по id? какое свойство является идентификатором? возможно, имена должны совпадать?

Итак, что происходит, так это то, что оригинальная коллекция на вашей сущности полностью заменена совершенно новой коллекцией, состоящей из новых экземпляров элементов. Во многих ситуациях это не будет проблемой, но когда вы объедините это с отслеживанием изменений в Entity Framework, вы теперь сообщите, что вся оригинальная коллекция должна быть удалена и заменена новым набором объектов. Очевидно, что это не то, что вы хотите.

Итак, как это решить? Ну, к сожалению, это немного боль. Первым шагом является указание AutoMapper полностью игнорировать коллекцию при отображении:

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

Обратите внимание, что я разбил это на две карты. Вам не нужно игнорировать коллекцию при сопоставлении с вашей моделью просмотра. Это не вызовет никаких проблем, потому что EF не отслеживает это. Это имеет значение только тогда, когда вы возвращаетесь к классу сущностей.

Но теперь вы не собираете эту коллекцию вообще, так как вы возвращаете значения к элементам? К сожалению, это ручной процесс:

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);
    }
}

Ответ 2

В соответствии с исходным файлом AutoMapper, который обрабатывает все ICollection (между прочим) и ICollection Mapper:

Коллекция очищается вызовом Clear(), а затем добавляется снова, так как я вижу, что AutoMapper не сможет автоматически выполнить сопоставление на этот раз.

Я бы выполнил некоторую логику, чтобы перебирать коллекции и AutoMapper.Map те, которые являются тем же самым

Ответ 3

В то же время существует расширение AutoMapper для этой конкретной проблемы:

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

С AutoMapper.EF6/EFCore вы также можете автоматически генерировать все сравнения на равенство. Пожалуйста см. AutoMapper.Collection AutoMapper.EF6 или AutoMapper.Collection.EFCore