public class TestParent { public int TestParentID { get; set; } public string Name { get; set; } public string Comment { get; set; } public virtual ICollection<TestChild> TestChildren { get; set; } } public class TestChild { public int TestChildID { get; set; } public int TestParentID { get; set; } public string Name { get; set; } public string Comment { get; set; } public virtual TestParent TestParent { get; set; } }
使用数据库中的数据填充对象效果很好.所以我可以在我的代码中使用testParent.TestChildren.OrderBy(tc => tc.Name).First().Name等.
然后我为testParents构建了一个标准的编辑表单.控制器看起来像这样:
public class TestController : Controller { private EFDbTestParentRepository testParentRepository = new EFDbTestParentRepository(); private EFDbTestChildRepository testChildRepository = new EFDbTestChildRepository(); public ActionResult ListParents() { return View(testParentRepository.TestParents); } public ViewResult EditParent(int testParentID) { return View(testParentRepository.TestParents.First(tp => tp.TestParentID == testParentID)); } [HttpPost] public ActionResult EditParent(TestParent testParent) { if (ModelState.IsValid) { testParentRepository.SaveTestParent(testParent); TempData["message"] = string.Format("Changes to test parents have been saved: {0} (ID = {1})",testParent.Name,testParent.TestParentID); return RedirectToAction("ListParents"); } // something wrong with the data values return View(testParent); } }
当表单回发到服务器时,模型绑定似乎运行良好 – 即testParent看起来没问题(id,name和comment按预期设置).但是导航属性TestChildren保持为NULL.
我猜这并不令人惊讶,因为模型绑定只是提取表单值,因为它们是从浏览器发送的,并将它们推送到TestParent类的对象中.然而,填充testParent.TestChildren需要立即往返数据库,这是实体框架的责任. EF可能不参与绑定过程.
然而,当我调用testParent.TestChildren.First()时,我期待延迟加载.相反,它会导致ArgumentNullException.
是否有必要在模型绑定后以特殊方式标记对象,以便实体框架将进行延迟加载?我怎样才能做到这一点?
显然,我可以使用第二个存储库testChildRepository手动检索子项.但是(a)感觉不对,(b)导致我的存储库设置方式出现问题(每个都使用自己的DBContext – 这是一个我还没有达成协议的问题).
解决方法
>父实体必须附加到EF上下文
>您的父实体必须是延迟加载代理
如果通过上下文从数据库加载父实体(并且导航属性是虚拟的以允许代理创建),则满足这两个要求.
如果您不从数据库加载实体但是手动创建它,则可以使用适当的EF函数实现相同的功能:
var parent = context.TestParents.Create(); parent.TestParentID = 1; context.TestParents.Attach(parent);
使用Create而不是new在这里很重要,因为它创建了所需的延迟加载代理.然后,您可以访问子集合,ID = 1的父节点的子节点将被懒惰地加载:
var children = parent.TestChildren; // no NullReferenceException
现在,默认的模型绑定器对这些特定的EF函数没有任何线索,并且只是用new实例化父级,也不会将它附加到任何上下文.这两个要求都没有得到满足,延迟加载也无法工作.
您可以使用Create()编写自己的模型绑定器来创建实例,但这可能是最糟糕的解决方案,因为它会使您的视图层非常依赖于EF.
如果在模型绑定之后需要子集合,我会在这种情况下通过显式加载来加载它:
// parent comes as parameter from POST action method context.TestParents.Attach(parent); context.Entry(parent).Collection(p => p.TestChildren).Load();
如果您的上下文和EF隐藏在存储库后面,则需要一个新的存储库方法,例如:
void LoadNavigationCollection<TElement>(T entity,Expression<Func<T,ICollection<TElement>>> navigationProperty) where TElement : class { _context.Set<T>().Attach(entity); _context.Entry(entity).Collection(navigationProperty).Load(); }
…其中_context是存储库类的成员.
但是,正如Darin所提到的,更好的方法是绑定viewmodels,然后根据需要将它们映射到您的实体.然后,您可以选择使用Create()实例化实体.