使用AutoMapper从MVC中的ViewModel更新实体

J86*_*J86 9 c# entity-framework automapper asp.net-mvc-5

我有一个Supplier.cs实体及其ViewModel SupplierVm.cs.我正在尝试更新现有供应商,但我收到黄色死亡屏幕(YSOD),并显示错误消息:

操作失败:无法更改关系,因为一个或多个外键属性不可为空.当对关系进行更改时,相关的外键属性将设置为空值.如果外键不支持空值,则必须定义新关系,必须为外键属性分配另一个非空值,或者必须删除不相关的对象.

我想我知道它为什么会发生,但我不知道如何解决它.这是一个关于正在发生的事情的截屏视频.我认为我得到错误的原因是因为当AutoMapper做它的事情时,这种关系就会丢失.

码

以下是我认为相关的实体:

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; }
}
Run Code Online (Sandbox Code Playgroud)

这是我的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";
}
Run Code Online (Sandbox Code Playgroud)

我的AutoMapper映射配置如下:

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);
Run Code Online (Sandbox Code Playgroud)

最后,这是我的SupplierController编辑方法:

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

  _supplierService.UpdateSupplier(supplier);
  return RedirectToAction("Index");
}
Run Code Online (Sandbox Code Playgroud)

以下是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();
}
Run Code Online (Sandbox Code Playgroud)

我已经做了很多阅读,根据这篇博文,我的工作应该有效!我也读过这样的东西,但我想在放弃使用AutoMapper更新实体之前先与读者联系.

Ger*_*old 8

原因

这条线......

Mapper.Map(supplier, updatedSupplier);
Run Code Online (Sandbox Code Playgroud)

......做的远不止眼睛.

  1. 在映射操作期间,懒惰地updatedSupplier加载其集合(Addresses等),因为AutoMapper(AM)访问它们.您可以通过监视SQL语句来验证这一点.
  2. AM 通过从视图模型映射的集合替换这些已加载的集合.尽管UseDestinationValue设置如此,但仍会发生 (就个人而言,我认为这个设置是不可理解的.)

这种替换有一些意想不到的后果

  1. 它将原始项留在附加到上下文的集合中,但不再在您所使用的方法的范围内.这些项仍然在Local集合中(如context.Addresses.Local)但现在被剥夺了它们的父项,因为EF已经执行了关系修复.他们的状态是Modified.
  2. 它将视图模型中的项目附加到Added状态中的上下文.毕竟,他们是新的背景.如果此时您希望1 Address英寸context.Addresses.Local,您会看到2.但您只能在调试器中看到添加的项目.

这些导致异常的父级"修改"项目就是这样.如果没有,那么下一个惊喜就是你只需要更新就可以在数据库中添加新项目.

好的,现在怎么样?

那你怎么解决这个问题呢?

答:我试图尽可能地重播你的场景.对我来说,一个可能的修复包括两个修改:

  1. 禁用延迟加载.我不知道你会如何安排你的存储库,但在某个地方应该有一个像这样的行

    context.Configuration.LazyLoadingEnabled = false;
    
    Run Code Online (Sandbox Code Playgroud)

    这样做,你只会有Added物品,而不是隐藏的Modified物品.

  2. 将Added项目标记为Modified.再一次,"某处",放线条

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

    ... 等等.

B.另一种选择是将视图模型映射到新的实体对象......

  var updatedSupplier = Mapper.Map<Supplier>(supplier);
Run Code Online (Sandbox Code Playgroud)

......并将其及其所有孩子标记为Modified.这在更新方面相当"昂贵",请参阅下一点.

C.在我看来,更好的解决方法是将AM从等式中完全取出并手动绘制状态.我总是担心在复杂的映射场景中使用AM.首先,因为映射本身的定义距离使用它的代码很远,所以使代码难以检查.但主要是因为它带来了自己的做事方式.并不总是清楚它是如何与其他微妙的操作相互作用的 - 比如变化跟踪.

绘画状态是一个艰苦的过程.基础可能是......如......

context.Entry(updatedSupplier).CurrentValues.SetValues(supplier);
Run Code Online (Sandbox Code Playgroud)

... 如果名称匹配supplier,updatedSupplier则复制标量属性.或者你可以使用AM(毕竟)将各个视图模型映射到它们的实体对应物,但忽略导航属性.

选项C为您提供了对最初预期更新内容的细粒度控制,而不是对选项B的全面更新.如有疑问,这可以帮助您确定使用哪个选项.