Обновить объект из ViewModel в MVC с помощью AutoMapper

У меня есть Supplier.cs Entity и его ViewModel SupplierVm.cs. Я пытаюсь обновить существующий Поставщик, но я получаю Желтый Экран Смерти (YSOD) с сообщением об ошибке:

Операция завершилась неудачно: отношение не может быть изменено, поскольку одно или несколько свойств внешнего ключа не имеют значения NULL. Когда происходит изменение отношения, соответствующее свойство внешнего ключа устанавливается равным нулевому значению. Если внешний ключ не поддерживает нулевые значения, необходимо определить новое отношение, для свойства внешнего ключа должно быть назначено другое ненулевое значение, или не связанный с ним объект должен быть удален.

Я думаю Я знаю, почему это происходит, но я не уверен, как его исправить.. Здесь screencast того, что происходит. Я думаю, причина, по которой я получаю ошибку, состоит в том, что эти отношения теряются, когда AutoMapper делает свою работу.

CODE

Вот Сущности, которые, по моему мнению, актуальны:

public abstract class Business : IEntity
{
  public int Id { get; set; }
  public string Name { get; set; }
  public string TaxNumber { get; set; }
  public string Description { get; set; }
  public string Phone { get; set; }
  public string Website { get; set; }
  public string Email { get; set; }
  public bool IsDeleted { get; set; }
  public DateTime CreatedOn { get; set; }
  public DateTime? ModifiedOn { get; set; }
  public virtual ICollection<Address> Addresses { get; set; } = new List<Address>();
  public virtual ICollection<Contact> Contacts { get; set; } = new List<Contact>();
}

public class Supplier : Business
{
  public virtual ICollection<PurchaseOrder> PurchaseOrders { get; set; }
}

public class Address : IEntity
{
  public Address()
  {
    CreatedOn = DateTime.UtcNow;
  }

  public int Id { get; set; }
  public string AddressLine1 { get; set; }
  public string AddressLine2 { get; set; }
  public string Area { get; set; }
  public string City { get; set; }
  public string County { get; set; }
  public string PostCode { get; set; }
  public string Country { get; set; }
  public bool IsDeleted { get; set; }
  public DateTime CreatedOn { get; set; }
  public DateTime? ModifiedOn { get; set; }
  public int BusinessId { get; set; }
  public virtual Business Business { get; set; }
}

public class Contact : IEntity
{
  public Contact()
  {
    CreatedOn = DateTime.UtcNow;
  }

  public int Id { get; set; }
  public string Title { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public string Phone { get; set; }
  public string Email { get; set; }
  public string Department { get; set; }
  public bool IsDeleted { get; set; }
  public DateTime CreatedOn { get; set; }
  public DateTime? ModifiedOn { get; set; }

  public int BusinessId { get; set; }
  public virtual Business Business { get; set; }
}

И вот мой ViewModel:

public class SupplierVm
{
  public SupplierVm()
  {
    Addresses = new List<AddressVm>();
    Contacts = new List<ContactVm>();
    PurchaseOrders = new List<PurchaseOrderVm>();
  }

  public int Id { get; set; }
  [Required]
  [Display(Name = "Company Name")]
  public string Name { get; set; }
  [Display(Name = "Tax Number")]
  public string TaxNumber { get; set; }
  public string Description { get; set; }
  public string Phone { get; set; }
  public string Website { get; set; }
  public string Email { get; set; }
  [Display(Name = "Status")]
  public bool IsDeleted { get; set; }

  public IList<AddressVm> Addresses { get; set; }
  public IList<ContactVm> Contacts { get; set; }
  public IList<PurchaseOrderVm> PurchaseOrders { get; set; }

  public string ButtonText => Id != 0 ? "Update Supplier" : "Add Supplier";
}

Конфигурация отображения AutoMapper My выглядит следующим образом:

cfg.CreateMap<Supplier, SupplierVm>();
cfg.CreateMap<SupplierVm, Supplier>()
  .ForMember(d => d.Addresses, o => o.UseDestinationValue())
  .ForMember(d => d.Contacts, o => o.UseDestinationValue());
cfg.CreateMap<Contact, ContactVm>();
cfg.CreateMap<ContactVm, Contact>()
  .Ignore(c => c.Business)
  .Ignore(c => c.CreatedOn);
cfg.CreateMap<Address, AddressVm>();
cfg.CreateMap<AddressVm, Address>()
  .Ignore(a => a.Business)
  .Ignore(a => a.CreatedOn);

Наконец, здесь мой метод SupplierController:

[HttpPost]
public ActionResult Edit(SupplierVm supplier)
{
  if (!ModelState.IsValid) return View(supplier);

  _supplierService.UpdateSupplier(supplier);
  return RedirectToAction("Index");
}

И здесь метод UpdateSupplier на SupplierService.cs:

public void UpdateSupplier(SupplierVm supplier)
{
  var updatedSupplier = _supplierRepository.Find(supplier.Id);
  Mapper.Map(supplier, updatedSupplier); // I lose navigational property here
  _supplierRepository.Update(updatedSupplier);
  _supplierRepository.Save();
}

Я сделал нагрузку на чтение и, согласно это сообщение в блоге, то, что я должен работать! Я также читал такие вещи, как это, но я думал, что я проверил бы с читателями, прежде чем переключать AutoMapper для обновления сущностей.

Ответ 1

Причина

Линия...

Mapper.Map(supplier, updatedSupplier);

... намного больше, чем кажется на первый взгляд.

  • Во время операции сопоставления updatedSupplier лениво загружает свои коллекции (Addresses и т.д.), потому что AutoMapper (AM) обращается к ним. Вы можете проверить это, отслеживая инструкции SQL.
  • AM заменяет эти загруженные коллекции коллекциями, которые он отображает из модели представления. Это происходит, несмотря на настройку UseDestinationValue. (Лично я считаю, что этот параметр непонятен.)

Эта замена имеет некоторые неожиданные последствия:

  1. Он оставляет исходные элементы в наборах, прикрепленных к контексту, но больше не входит в объем метода, в котором вы находитесь. Элементы все еще находятся в коллекциях Local (например, context.Addresses.Local), но теперь лишены их parent, потому что EF выполнил исправление отношения. Их состояние Modified.
  2. Он прикрепляет элементы из модели представления к контексту в состоянии Added. В конце концов, они новы к контексту. Если в этот момент вы ожидаете 1 Address в context.Addresses.Local, вы увидите 2. Но вы видите только добавленные элементы в отладчике.

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

Хорошо, теперь что?

Итак, как вы это исправите?

A. Я попытался воспроизвести ваш сценарий как можно ближе. Для меня одно возможное исправление состояло из двух модификаций:

  • Отключить ленивую загрузку. Я не знаю, как вы могли бы организовать это с вашими репозиториями, но где-то там должна быть строка типа

    context.Configuration.LazyLoadingEnabled = false;
    

    Выполняя это, вы будете иметь только элементы Added, а не скрытые элементы Modified.

  • Отметьте Added элементы как Modified. Опять же, "где-то", введите строки типа

    foreach (var addr in updatedSupplier.Addresses)
    {
        context.Entry(addr).State = System.Data.Entity.EntityState.Modified;
    }
    

    ... и т.д.

B. Другой вариант - сопоставить модель представления с объектами новых объектов...

  var updatedSupplier = Mapper.Map<Supplier>(supplier);

... и отметьте его и всех его детей как Modified. Это довольно "дорого" в плане обновлений, см. Следующий пункт.

C. Лучшее решение, на мой взгляд, - полностью вывести AM из уравнения и нарисовать его вручную. Я всегда опасаюсь использовать AM для сложных сценариев сопоставления. Во-первых, потому что само отображение определено далеко от кода, где он использовался, что затрудняет проверку кода. Но главным образом потому, что он приносит свои собственные способы делать вещи. Не всегда ясно, как он взаимодействует с другими деликатными операциями - например, отслеживанием изменений.

Живопись состояния - это кропотливая процедура. Основой может быть утверждение вроде...

context.Entry(updatedSupplier).CurrentValues.SetValues(supplier);

... копирует supplier скалярные свойства в updatedSupplier, если их имена совпадают. Или вы можете использовать AM (в конце концов) для сопоставления отдельных моделей представлений с их эквивалентами сущностей, но игнорируя свойства навигации.

Вариант C дает вам мелкомасштабный контроль над тем, что обновляется, как вы изначально планировали, вместо широкого обновления опции B. Если у вас есть сомнения, this может помогите вам решить, какой вариант использовать.

Ответ 2

Я получил эту проблему много раз и обычно это:

Идентификатор FK в исходной ссылке не соответствует PK для этого объекта FK. т.е. если у вас есть таблица заказов и таблица OrderStatus. Когда вы загружаете оба объекта, Order имеет OrderStatusId = 1 и OrderStatus.Id = 1. Если вы измените OrderStatusId = 2, но не обновите OrderStatus.Id до 2, вы получите эту ошибку. Чтобы исправить это, вам нужно либо загрузить идентификатор 2, либо обновить ссылочный объект, либо просто установить контрольный объект OrderStatus на порядок до нуля до сохранения.

Ответ 3

Я не уверен, что это будет соответствовать вашим требованиям, но я бы предложил следующее.

Из вашего кода, похоже, вы теряете отношения во время сопоставления.

Мне кажется, что в рамках операции UpdateSupplier вы фактически не обновляете ни одну из дочерних деталей поставщика.

Если это так, я бы предложил обновить только измененные свойства от поставщикаVVV до класса поставщика. Вы можете написать отдельный метод, в котором вы присваиваете значения свойств от поставщикаVV к объекту поставщика (это должно изменять только свойства, отличные от детей, такие как имя, описание, веб-сайт, телефон и т.д.).

И затем выполните обновление db. Это избавит вас от возможных ошибок отслеживаемых объектов.

Если вы меняете дочерние сущности поставщика, я бы предложил обновить их независимо от поставщиков, потому что для извлечения всего графа объектов из базы данных потребуется много запросов, которые будут выполняться, и обновление будет также выполнять ненужные запросы обновления в базе данных.

Обновление сущностей независимо могло бы сэкономить много операций db и добавить к производительности приложения.

Вы все равно можете использовать поиск всего графа объектов, если вам нужно отобразить все детали о поставщике на одном экране. Для обновлений я бы не рекомендовал обновление всего массива объектов.

Надеюсь, это поможет решить вашу проблему.