C# 实体框架4.3,MVC处于编辑状态';无法保存复杂对象

C# 实体框架4.3,MVC处于编辑状态';无法保存复杂对象,c#,asp.net-mvc,asp.net-mvc-3,entity-framework,entity-framework-4.3,C#,Asp.net Mvc,Asp.net Mvc 3,Entity Framework,Entity Framework 4.3,我用Northwind数据库做了一个小项目来说明这个问题 以下是控制器的操作: [HttpPost] public ActionResult Edit(Product productFromForm) { try { context.Products.Attach(productFromForm); var fromBD = context.Categories.Find(productFromForm.Category.CategoryID);

我用Northwind数据库做了一个小项目来说明这个问题

以下是控制器的操作:

[HttpPost]
public ActionResult Edit(Product productFromForm)
{
    try
    {
        context.Products.Attach(productFromForm);
        var fromBD = context.Categories.Find(productFromForm.Category.CategoryID);
        productFromForm.Category = fromBD;
        context.Entry(productFromForm).State = EntityState.Modified;
        context.SaveChanges();
        return RedirectToAction("Index");
    }
    catch
    {
        return View();
    }
}
上下文在控制器的构造函数中实例化为
newdatabasecontext()

公共类数据库上下文:DbContext
{
公共数据库上下文()
:base(“应用程序服务”){
base.Configuration.ProxyCreationEnabled=false;
base.Configuration.LazyLoadingEnabled=false;
}
公共数据库集产品{get;set;}
公共数据库集类别{get;set;}
模型创建时受保护的覆盖无效(DbModelBuilder modelBuilder){
modelBuilder.Configurations.Add(新产品配置());
modelBuilder.Configurations.Add(新类别配置());
}
私有类ProductConfiguration:EntityTypeConfiguration{
公共产品配置(){
ToTable(“产品”);
HasKey(p=>p.ProductID);
has可选(p=>p.Category)。有许多(x=>x.Products)。Map(c=>c.MapKey(“CategoryID”);
房地产(p=>p.UnitPrice).HasColumnType(“货币”);
}
}
私有类类别配置:EntityTypeConfiguration{
公共类别配置(){
ToTable(“类别”);
HasKey(p=>p.CategoryID);
}
}
}
公共类类别{
public int CategoryID{get;set;}
公共字符串CategoryName{get;set;}
公共字符串说明{get;set;}
公共虚拟ICollection产品{get;set;}
}
公共类产品{
public int ProductID{get;set;}
公共字符串ProductName{get;set;}
公共字符串QuantityPerUnit{get;set;}
公共十进制单价{get;set;}
public Int16 UnitsInStock{get;set;}
公共Int16 UnitsOrder{get;set;}
公共Int16重新排序级别{get;set;}
公共布尔已中断{get;set;}
公共虚拟类别{get;set;}
}
问题是我可以保存产品中的任何内容,但不能保存类别的更改

对象productFromForm包含productFromForm.Product.ProductID中的新CategoryID,没有问题。但是,当我
Find()
从上下文中检索对象的类别时,我有一个没有名称和描述的对象(两者都保持为NULL),并且
SaveChanges()
即使属性
类别的ID已更改,也不会修改引用


知道为什么吗?

问题是EF跟踪关联更新的方式不同于值类型。执行此操作时,
context.Products.Attach(productFromForm),productFromForm只是一个不跟踪任何更改的poco。当您将其标记为已修改时,EF将更新所有值类型,但不会更新关联

更常见的方法是:

[HttpPost]
public ActionResult Edit(Product productFromForm)
{
    // Might need this - category might get attached as modified or added
    context.Categories.Attach(productFromForm.Category);

    // This returns a change-tracking proxy if you have that turned on.
    // If not, then changing product.Category will not get tracked...
    var product = context.Products.Find(productFromForm.ProductId);

    // This will attempt to do the model binding and map all the submitted 
    // properties to the tracked entitiy, including the category id.
    if (TryUpdateModel(product))  // Note! Vulnerable to overposting attack.
    {
        context.SaveChanges();
        return RedirectToAction("Index");
    }

    return View();
}
我发现的最不容易出错的解决方案有两个方面,尤其是当模型变得更复杂时:

  • 对任何输入使用DTO(类ProductInput)。然后使用AutoMapper之类的工具将数据映射到域对象。在您开始提交越来越复杂的数据时尤其有用
  • 在域对象中显式声明外键。例如,添加一个类别来制作您的产品。将输入映射到此属性,而不是关联对象。并进一步解释这一点。独立关联和外键都有各自的问题,但到目前为止,我发现外键方法没有那么麻烦(例如,关联实体标记为已添加、连接顺序、映射前跨数据库关注点等)

    • 您(显然)更改的关系不会被保存,因为您没有真正更改关系:

      context.Products.Attach(productFromForm);
      
      此行将
      productFromForm
      productFromForm.Category
      附加到上下文

      var fromBD = context.Categories.Find(productFromForm.Category.CategoryID);
      
      此行返回附加的对象
      productFromForm.Category
      ,而不是数据库中的对象

      productFromForm.Category = fromBD;
      
      这条线指定了同一个对象,因此它什么也不做

      context.Entry(productFromForm).State = EntityState.Modified;
      
      此行仅影响
      productFromForm
      的标量属性,而不影响任何导航属性

      更好的办法是:

      // Get original product from DB including category
      var fromBD = context.Products
          .Include(p => p.Category)  // necessary because you don't have a FK property
          .Single(p => p.ProductId == productFromForm.ProductId);
      
      // Update scalar properties of product
      context.Entry(fromBD).CurrentValues.SetValues(productFromForm);
      
      // Update the Category reference if the CategoryID has been changed in the from
      if (productFromForm.Category.CategoryID != fromBD.Category.CategoryID)
      {
          context.Categories.Attach(productFromForm.Category);
          fromBD.Category = productFromForm.Category;
      }
      
      context.SaveChanges();
      
      如果您在模型中将外键作为属性公开,这将变得容易得多——正如@Lenence的答案和您之前问题的答案中所述。使用FK属性(并假设您将
      Product.CategoryID
      直接绑定到视图,而不是
      Product.Category.CategoryID
      ),上述代码简化为:

      var fromBD = context.Products
          .Single(p => p.ProductId == productFromForm.ProductId);
      context.Entry(fromBD).CurrentValues.SetValues(productFromForm);
      context.SaveChanges();
      
      或者,您可以将状态设置为
      Modified
      ,这将适用于FK属性:

      context.Entry(productFromForm).State = EntityState.Modified;
      context.SaveChanges();
      

      如果您只是设置product.CategoryID而不是类别导航引用,会发生什么情况?productFromForm已经包含表单中正确填写的.CategoryID。啊,我没有直接在控制器中使用我的实体,所以我不确定。谢谢。目前,我们决定不使用外键之类的数据库内容来修改模型。我知道编写代码要少一些,但这是团队目前的决定。这个解决方案很有效,解释也很可靠。干得好您认为将0..1关系的类别设置为NULL是否适用于此类代码?@PatrickDesjardins:是的,我认为它会起作用。在将引用设置为null之前,您只需使用
      Include
      加载原始文件。我做了一个测试,它也可以工作。非常感谢您的回答和它的质量。
      var fromBD = context.Products
          .Single(p => p.ProductId == productFromForm.ProductId);
      context.Entry(fromBD).CurrentValues.SetValues(productFromForm);
      context.SaveChanges();
      
      context.Entry(productFromForm).State = EntityState.Modified;
      context.SaveChanges();