![](/img/trans.png)
[英]ASP.net MVC - Should I use AutoMapper from ViewModel to Entity Framework entities?
[英]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);
... 做的远不止眼前一亮。
updatedSupplier
延迟加载其集合( Addresses
等),因为 AutoMapper (AM) 访问它们。 您可以通过监视 SQL 语句来验证这一点。UseDestinationValue
但UseDestinationValue
发生这种情况。 (个人觉得这个设定看不懂。)这种替换有一些意想不到的后果:
Local
集合中(如context.Addresses.Local
)但现在被剥夺了它们的父项,因为EF 已执行关系修正。 他们的状态是Modified
。Added
状态的上下文。 毕竟,他们对上下文不熟悉。 如果此时您希望context.Addresses.Local
1 个Address
,您会看到 2 个。但您只能在调试器中看到添加的项目。正是这些没有父项的“修改”项导致了异常。 如果没有,下一个惊喜就是您将新项目添加到数据库中,而您只希望更新。
那么你如何解决这个问题?
答:我试图尽可能地重播你的场景。 对我来说,一个可能的修复包括两个修改:
禁用延迟加载。 我不知道你会如何用你的存储库安排它,但在某处应该有一条线
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>(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.