我有一个Supplier.cs
实体及其ViewModel SupplierVm.cs
。我正在尝试更新现有供应商,但我收到黄色死亡屏幕(YSOD)并显示错误消息:
操作失败:无法更改关系,因为一个或多个外键属性不可为空。当对关系进行更改时,相关的外键属性将设置为空值。如果外键不支持空值,则必须定义新关系,必须为外键属性分配另一个非空值,或者必须删除不相关的对象。
我想想我知道它为什么会发生,但我不确定如何修复它。这是screencast正在发生的事情。我认为我收到错误的原因是因为当AutoMapper 它的东西时,这种关系就会丢失。
CODE
以下是我认为相关的实体:
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");
}
这里是UpdateSupplier
上的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();
}
我已经完成了大量阅读,根据this blog post,我的工作应该有效!我还读过stuff like this,但我想在放弃使用AutoMapper更新实体之前先与读者核对。
答案 0 :(得分:7)
线......
Mapper.Map(supplier, updatedSupplier);
......远不止眼睛。
updatedSupplier
懒惰地加载其集合(Addresses
等),因为AutoMapper(AM)访问它们。您可以通过监视SQL语句来验证这一点。UseDestinationValue
设置,但仍会发生这种情况。 (就个人而言,我认为这种设置是不可理解的。)这种替代会产生一些意想不到的后果:
Local
个集合中(例如context.Addresses.Local
)但现在被剥夺了他们的父母,因为EF已经执行了关系修正。他们的州是Modified
。Added
状态的上下文中。毕竟,他们对上下文不熟悉。如果此时您在Address
中预计会有context.Addresses.Local
,那么您会看到2.但您只能看到调试器中添加的项目。这些导致异常的无父&#39; Modified`项目。如果它没有,那么下一个意外就是你只需要更新就可以向数据库添加新项目。
那你怎么解决这个问题?
A。我尝试尽可能地重播您的方案。对我来说,一个可能的修复包括两个修改:
禁用延迟加载。我不知道你会如何安排你的存储库,但在某个地方应该有一个像
这样的行context.Configuration.LazyLoadingEnabled = false;
执行此操作后,您只会拥有Added
项,而不会隐藏Modified
项。
将Added
项标记为Modified
。再次,&#34;某处&#34;,放置像
foreach (var addr in updatedSupplier.Addresses)
{
context.Entry(addr).State = System.Data.Entity.EntityState.Modified;
}
......等等。
B。另一种选择是将视图模型映射到新的实体对象......
var updatedSupplier = Mapper.Map<Supplier>(supplier);
...并将其及其所有子项标记为Modified
。这非常昂贵&#34;但就更新而言,请参阅下一点。
C。我认为更好的解决方法是将AM完全排除在外,并且手动绘制状态。我总是对使用AM进行复杂的映射方案持谨慎态度。首先,因为映射本身的定义距离使用它的代码很远,所以使代码难以检查。但主要是因为它带来了自己的做事方式。它并不总是清楚它是如何与其他微妙的操作相互作用的 - 比如变化跟踪。
绘制国家是一个艰苦的过程。基础可以是像...这样的陈述。
context.Entry(updatedSupplier).CurrentValues.SetValues(supplier);
...如果名称匹配,则会将supplier
的标量属性复制到updatedSupplier
。或者你可以使用AM(毕竟)将各个视图模型映射到它们的实体对应物,但忽略导航属性。
选项C可让您对最初预期更新的内容进行细粒度控制,而不是对选项B的全面更新。如有疑问,this可帮助您确定使用哪个选项。
答案 1 :(得分:1)
我搜索了所有stackoverflow答案和google搜索。最后我只是添加了'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(); }
答案 2 :(得分:0)
我多次得到这个问题,通常是这样的:
父引用上的FK Id与该FK实体上的PK不匹配。即如果您有Order表和OrderStatus表。当您将两者加载到实体中时,Order具有OrderStatusId = 1且OrderStatus.Id = 1.如果更改OrderStatusId = 2但不将OrderStatus.Id更新为2,则会出现此错误。要修复它,您需要加载Id of 2并更新引用实体,或者在保存之前将Order上的OrderStatus引用实体设置为null。
答案 3 :(得分:0)
我不确定这是否符合您的要求,但我建议您遵循。
从你的代码中看,你肯定会在映射的某个地方失去关系。
对我而言,作为UpdateSupplier操作的一部分,您实际上并未更新供应商的任何子详细信息。
如果是这种情况,我建议仅将更改的属性从SupplierVm更新为域供应商类。您可以编写一个单独的方法,将SupplierVm中的属性值分配给Supplier对象(这应该只更改非子属性,如Name,Description,Website,Phone等。)
然后执行db Update。这将使您免于被跟踪实体的混乱。
如果要更改供应商的子实体,我建议独立于供应商更新它们,因为从数据库中检索整个对象图需要执行大量查询,更新它也会对数据库执行不必要的更新查询。
独立更新实体将节省大量的数据库操作,并会增加应用程序的性能。
如果必须在一个屏幕中显示有关供应商的所有详细信息,您仍然可以使用整个对象图的检索。对于更新,我不建议更新整个对象图。
我希望这有助于解决您的问题。