是否真的不可能在EF中更新子集合(也就是非hacky方式)?

假设您的实体中有这些类。

public class Parent { public int ParentID { get; set; } public virtual ICollection Children { get; set; } } public class Child { public int ChildID { get; set; } public int ParentID { get; set; } public virtual Parent Parent { get; set; } } 

并且您有一个用户界面来更新Parent及其Children ,这意味着如果用户添加新Child则必须插入,如果用户编辑现有Child则需要更新,如果用户删除了Child那么必须删除。 现在很明显,如果您使用以下代码

 public void Update(Parent obj) { _parent.Attach(obj); _dbContext.Entry(obj).State = EntityState.Modified; _dbContext.SaveChanges(); } 

它将无法检测Child内部的更改,因为EF无法检测导航属性内的更改。

我一直在问这个问题4次,得到的答案很复杂。 那么实际上可以做这些事情而不会变得复杂吗? 这个问题可以通过分离ParentChild之间的用户界面来解决问题,但我不想这样做,因为在一个菜单中合并ChildParent在业务应用程序开发中非常常见且更加用户友好。

更新:我正在尝试下面的解决方案,但它不起作用。

 public ActionResult(ParentViewModel model) { var parentFromDB = context.Parent.Get(model.ParentID); if (parentFromDB != null) { parentFromDB.Childs = model.Childs; } context.SaveChanges(); } 

EF不会检测孩子内部的变化,也无法说明如何处理孩子。 例如,如果parentFromDB有3个孩子,我第一次从DB中取出它,那么我删除第2个和第3个孩子。 然后我得到了The relationship could not be changed because one or more of the foreign-key properties is non-nullable在保存时The relationship could not be changed because one or more of the foreign-key properties is non-nullable

我相信这就是发生的事情: 由于一个或多个外键属性不可为空,因此无法更改关系

这让我回到原点,因为在我的场景中,我不能只从数据库中获取并更新条目并调用SaveChanges

因为EF无法检测导航属性中的更改

这似乎是对_dbContext.Entry(obj).State = EntityState.Modified不会将navigaton属性标记为已修改的事实的有些扭曲的描述。

当然EF跟踪导航属性的变化。 它跟踪附加到上下文的所有实体的属性和关联的变化。 因此,你的问题的答案现在已经明确表示……

是否可以在EF中更新EF中的子集合

……是: 是的

唯一的事情是: 你不要开箱即用

更新任何实体的“开箱即用”方式,无论是父项还是某个集合中的子项,都是:

  • 从数据库中获取实体。
  • 修改其属性或向其集合添加/删除元素
  • 调用SaveChanges()

就这样。 如果跟踪更改,您永远不会明确设置实体State

但是,在断开连接(n层)的情况下,这会变得更复杂。 我们序列化和反序列化实体,因此不能有任何跟踪其更改的上下文。 如果我们想将实体存储在数据库中,那么现在我们的任务就是让EF了解更改。 基本上有两种方法可以做到这一点:

  • 根据我们对实体的了解手动设置状态(例如:主键> 0表示它们存在且应该更新)
  • 绘制状态 :从数据库中检索实体,然后将更改从反序列化的实体重新应用到它们。

在谈到协会时,我们总是要画国家 。 我们必须从数据库中获取当前实体,并确定添加/删除了哪些子项。 没有办法从反序列化的对象图本身推断出这一点。

有各种方法来缓解这种无聊和精心绘制国家的任务,但这超出了本问答的范围。 一些参考:

  • 用于更新整个聚合的通用存储库
  • GraphDiff

它可以让你做得很奇怪。

这需要延迟加载以获取孩子(显然根据您的使用情况进行修改)

//得到父母

 var parent = context.Parent.Where(x => x.Id == parentId).SingleOrDefault(); 

为你写了一个完整的测试方法。 (适用于您的情况)

EmailMessage(父级)是父级,它没有或很多EmailAttachment(子级)

  [TestMethod] public void TestMethodParentChild() { using (var context = new MyContext()) { //put some data in the Db which is linked //--------------------------------- var emailMessage = new EmailMessage { FromEmailAddress = "sss", Message = "test", Content = "hiehdue", ReceivedDateTime = DateTime.Now, CreateOn = DateTime.Now }; var emailAttachment = new EmailAttachment { EmailMessageId = 123, OrginalFileName = "samefilename", ContentLength = 3, File = new byte[123] }; emailMessage.EmailAttachments.Add(emailAttachment); context.EmailMessages.Add(emailMessage); context.SaveChanges(); //--------------------------------- var firstEmail = context.EmailMessages.FirstOrDefault(x => x.Content == "hiehdue"); if (firstEmail != null) { //change the parent if you want //foreach child change if you want foreach (var item in firstEmail.EmailAttachments) { item.OrginalFileName = "I am the shit"; } } context.SaveChanges(); } } 

更新

做你的AutoMappper Stuff ……正如你在评论中所说的那样。

然后当你准备保存并将它作为正确的类型返回时,即一次代表实体(Db)然后执行此操作。

 var modelParent= "Some auto mapper magic to get back to Db types." var parent = context.Parent.FirstOrDefault(x => x.Id == modelParent.Id); //use automapper here to update the parent again if (parent != null) { parent.Childs = modelParent.Childs; } //this will update all childs ie if its not in the new list from the return //it will automatically be deleted, if its new it will be added and if it // exists it will be updated. context.SaveChanges();