使用 AutoMapper 从 MVC 中的 ViewModel 更新实体 [英] Update Entity from ViewModel in MVC using AutoMapper

查看:25
本文介绍了使用 AutoMapper 从 MVC 中的 ViewModel 更新实体的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个 Supplier.cs 实体和它的 ViewModel SupplierVm.cs.我正在尝试更新现有的供应商,但我收到了带有错误消息的黄屏死机 (YSOD):

I have a Supplier.cs Entity and its ViewModel SupplierVm.cs. I am attempting to update an existing Supplier, but I am getting the Yellow Screen of Death (YSOD) with the error message:

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

The operation failed: The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.

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

I think I know why it is happening, but I'm not sure how to fix it. Here's a screencast of what is happening. I think the reason I'm getting the error is because that relationship is lost when AutoMapper does its thing.

代码

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

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 编辑方法:

Finally, here's my SupplierController Edit Method:

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

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

这里是 SupplierService.cs 上的 UpdateSupplier 方法:

And here's the UpdateSupplier Method on the 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 更新实体之前与读者核实.

I've done a load of reading and according to this blog post, what I have should work! I've also read stuff like this but I thought I'd check with readers before ditching AutoMapper for Updating Entities.

推荐答案

原因

线...

Mapper.Map(supplier, updatedSupplier);

...做的远不止眼前一亮.

... does a lot more than meets the eye.

  1. 在映射操作期间,updatedSupplier 会延迟加载其集合(Addresses 等),因为 AutoMapper (AM) 会访问它们.您可以通过监控 SQL 语句来验证这一点.
  2. AM 用它从视图模型映射的集合替换这些加载的集合.尽管有 UseDestinationValue 设置,但仍会发生这种情况.(个人觉得这个设定看不懂.)
  1. During the mapping operation, updatedSupplier loads its collections (Addresses, etc) lazily because AutoMapper (AM) accesses them. You can verify this by monitoring SQL statements.
  2. AM replaces these loaded collections by the collections it maps from the view model. This happens despite the UseDestinationValue setting. (Personally, I think this setting is incomprehensible.)

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

This replacement has some unexpected consequences:

  1. 它将集合中的原始项保留到上下文中,但不再处于您所在方法的范围内.这些项仍位于 Local 集合中(如 context.Addresses.Local) 但现在被剥夺了他们的父母,因为 EF 已经执行了关系修复.它们的状态是 Modified.
  2. 它将视图模型中的项目附加到处于 Added 状态的上下文.毕竟,他们对上下文不熟悉.如果此时您希望 context.Addresses.Local 中有 1 个 Address,那么您会看到 2.但您只能在调试器中看到添加的项目.莉>
  1. It leaves the original items in the collections attached to the context, but no longer in scope of the method you're in. The items are still in the Local collections (like context.Addresses.Local) but now deprived of their parent, because EF has executed relationship fixup. Their state is Modified.
  2. It attaches the items from the view model to the context in an Added state. After all, they're new to the context. If at this point you'd expect 1 Address in context.Addresses.Local, you'd see 2. But you only see the added items in the debugger.

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

It's these parent-less 'Modified` items that cause the exception. And if it didn't, the next surprise would have been that you add new items to the database while you only expected updates.

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

So how do you fix this?

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

A. I tried to replay your scenario as closely as possible. For me, one possible fix consisted of two modifications:

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

  1. Disable lazy loading. I don't know how you would arrange this with your repositories, but somewhere there should be a line like

context.Configuration.LazyLoadingEnabled = false;

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

Doing this, you'll only have the Added items, not the hidden Modified items.

Added 项标记为 Modified.再次,某处",像

Mark the Added items as Modified. Again, "somewhere", put lines like

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

...等等.

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

B. Another option is to map the view model to new entity objects ...

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

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

... and mark it, and all of its children, as Modified. This is quite "expensive" in terms of updates though, see the next point.

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

C. A better fix in my opinion is to take AM out of the equation completely and paint the state manually. I'm always wary of using AM for complex mapping scenarios. First, because the mapping itself is defined a long way away from the code where it's used, making code difficult to inspect. But mainly because it brings its own ways of doing things. It's not always clear how it interacts with other delicate operations --like change tracking.

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

Painting the state is a painstaking procedure. The basis could be a statement like ...

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

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

... which copies supplier's scalar properties to updatedSupplier if their names match. Or you could use AM (after all) to map individual view models to their entity counterparts, but ignoring the navigation properties.

选项 C 可让您按照最初的预期对更新内容进行细粒度控制,而不是选项 B 的全面更新.如有疑问, 可以帮助您决定使用哪个选项.

Option C gives you fine-grained control over what gets updated, as you originally intended, instead of the sweeping update of option B. When in doubt, this may help you decide which option to use.

这篇关于使用 AutoMapper 从 MVC 中的 ViewModel 更新实体的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆