使用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);
……做的远不止眼睛。
- 在映射操作期间,
updatedSupplier
懒洋洋地加载其集合(Addresses
等),因为AutoMapper(AM)访问它们。 您可以通过监视SQL语句来validation这一点。 - AM通过从视图模型映射的集合替换这些已加载的集合。 尽管使用了
UseDestinationValue
设置,但UseDestinationValue
发生这种情况。 (就个人而言,我认为这种设置是不可理解的。)
这种替换有一些意想不到的后果
- 它将原始项目保留在附加到上下文的集合中,但不再在您所使用的方法的范围内。这些项目仍在
Local
集合中(如context.Addresses.Local
)但现在已被剥夺其父项,因为EF已执行关系修正 。 他们的状态是Modified
。 - 它将视图模型中的项目附加到已
Added
状态的上下文中。 毕竟,他们是新的背景。 如果此时您希望在context.Addresses.Local
1个Address
,您会看到2.但是您只能在调试器中看到添加的项目。
这些导致exception的父级“修改”项目。 如果没有,那么下一个意外就是你只需要更新就可以向数据库中添加新项目。
好的,现在怎么样?
那你怎么解决这个问题呢?
答:我试图尽可能地重播你的场景。 对我来说,一个可能的修复包括两个修改:
-
禁用延迟加载。 我不知道你将如何安排你的存储库,但在某个地方应该有一个像这样的行
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);
…并将其及其所有子项标记为已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。 这将使您免于被跟踪实体的可能混乱。
如果要更改供应商的子实体,我建议独立于供应商更新它们,因为从数据库中检索整个对象图需要执行大量查询,更新它也会对数据库执行不必要的更新查询。
独立更新实体将节省大量的数据库操作,并会增加应用程序的性能。
如果必须在一个屏幕中显示有关供应商的所有详细信息,您仍然可以使用整个对象图的检索。 对于更新,我不建议更新整个对象图。
我希望这有助于解决您的问题。
上述就是C#学习教程:使用AutoMapper从MVC中的ViewModel更新实体分享的全部内容,如果对大家有所用处且需要了解更多关于C#学习教程,希望大家多多关注—计算机技术网(www.ctvol.com)!
本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。
ctvol管理联系方式QQ:251552304
本文章地址:https://www.ctvol.com/cdevelopment/961332.html