繁体   English   中英

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

[英]Update Entity from ViewModel in MVC using AutoMapper

我有一个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; }
}

这是我的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 映射配置是这样的:

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

这是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 语句来验证这一点。
  2. AM它从视图模型映射的集合替换这些加载的集合。 尽管设置了UseDestinationValueUseDestinationValue发生这种情况。 (个人觉得这个设定看不懂。)

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

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

正是这些没有父项的“修改”项导致了异常。 如果没有,下一个惊喜就是您将新项目添加到数据库中,而您只希望更新。

好的,现在呢?

那么你如何解决这个问题?

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

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

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

C.在我看来,更好的解决方法是将 AM 完全排除在等式之外并手动绘制状态 我总是对将 AM 用于复杂的映射场景持谨慎态度。 首先,因为映射本身的定义与使用它的代码相距很远,使得代码难以检查。 但主要是因为它带来了自己的做事方式。 它如何与其他精细操作(例如更改跟踪)交互并不总是很清楚。

绘制状态是一个艰苦的过程。 基础可以是像...

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

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

选项 C 使您可以按照您最初的意图对要更新的内容进行细粒度控制,而不是选项 B 的全面更新。如有疑问,可能有助于您决定使用哪个选项。

我搜索了所有 stackoverflow 答案和谷歌搜索。 最后我刚刚添加了'db.Configuration.LazyLoadingEnabled = false;' 线,它对我来说非常有效。

 var message = JsonConvert.DeserializeObject<UserMessage>(@"{.....}"); using (var db = new OracleDbContex()) { db.Configuration.LazyLoadingEnabled = false; var msguser = Mapper.Map<BAPUSER>(message); var dbuser = db.BAPUSER.FirstOrDefault(w => w.BAPUSERID == 1111); Mapper.Map(msguser, dbuser); // db.Entry(userx).State = EntityState.Modified; db.SaveChanges(); }

我已经多次遇到这个问题,通常是这样的:

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

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

从您的代码来看,您在某处映射期间肯定会失去关系。

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

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

然后执行数据库更新。 这将使您免于被跟踪实体可能出现的混乱。

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

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

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM