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

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

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

我知道它为什么会发生,但我不知道如何解决它 。 这是一个关于正在发生的事情的截屏video 。 我认为我收到错误的原因是因为当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
Addresses { get; set; } = new List
(); public virtual ICollection Contacts { get; set; } = new List(); } public class Supplier : Business { public virtual ICollection 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(); Contacts = new List(); PurchaseOrders = new List(); } 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 Addresses { get; set; } public IList Contacts { get; set; } public IList PurchaseOrders { get; set; } public string ButtonText => Id != 0 ? "Update Supplier" : "Add Supplier"; } 

我的AutoMapper映射配置如下:

 cfg.CreateMap(); cfg.CreateMap() .ForMember(d => d.Addresses, o => o.UseDestinationValue()) .ForMember(d => d.Contacts, o => o.UseDestinationValue()); cfg.CreateMap(); cfg.CreateMap() .Ignore(c => c.Business) .Ignore(c => c.CreatedOn); cfg.CreateMap
(); cfg.CreateMap() .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"); } 

这是SupplierService.cs上的UpdateSupplier方法:

 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更新实体之前先与读者联系。

原因

线……

 Mapper.Map(supplier, updatedSupplier); 

……做的远不止眼睛。

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

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

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

这些导致exception的父级“修改”项目。 如果没有,那么下一个意外就是你只需要更新就可以向数据库中添加新项目。

好的,现在怎么样?

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

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

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

     context.Configuration.LazyLoadingEnabled = false; 

    这样做,您将只有已Added项目,而不是隐藏的已Modified项目。

  2. Added项目标记为已Modified 。 再一次,“某处”,放线条

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

    … 等等。

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

  var updatedSupplier = Mapper.Map(supplier); 

…并将其及其所有子项标记为已Modified 。 这在更新方面相当“昂贵”,请参阅下一点。

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

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

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

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

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

我多次得到这个问题,通常是这样的:

父引用上的FK Id与该FK实体上的PK不匹配。 即如果您有一个Order表和一个OrderStatus表。 当您将两者加载到实体中时,Order具有OrderStatusId = 1且OrderStatus.Id = 1.如果更改OrderStatusId = 2但不将OrderStatus.Id更新为2,则会出现此错误。 要修复它,您需要加载Id of 2并更新引用实体,或者在保存之前将Order上的OrderStatus引用实体设置为null。

我不确定这是否符合您的要求但我建议遵循。

从你的代码来看,它确实看起来你在映射某个地方时失去了关系。

对我而言,作为UpdateSupplier操作的一部分,您实际上并未更新供应商的任何子详细信息。

如果是这种情况,我建议仅将更改的属性从SupplierVm更新为域供应商类。 您可以编写一个单独的方法,将SupplierVm中的属性值分配给Supplier对象(这应该只更改非子属性,如Name,Description,Website,Phone等)。

然后执行db Update。 这将使您免于被跟踪实体的可能混乱。

如果要更改供应商的子实体,我建议独立于供应商更新它们,因为从数据库中检索整个对象图需要执行大量查询,更新它也会对数据库执行不必要的更新查询。

独立更新实体将节省大量的数据库操作,并会增加应用程序的性能。

如果必须在一个屏幕中显示有关供应商的所有详细信息,您仍然可以使用整个对象图的检索。 对于更新,我不建议更新整个对象图。

我希望这有助于解决您的问题。