简体   繁体   中英

Entity Framework 4.3 with MVC on Edit doesn't save complex object

I made a small project with Northwind database to illustrate the problematic.

Here is the action of the controller :

[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();
    }
}

context is instanced in the constructor of the Controller as new DatabaseContext() .

public class DatabaseContext:DbContext
{
    public DatabaseContext()
        : base("ApplicationServices") {
        base.Configuration.ProxyCreationEnabled = false;
        base.Configuration.LazyLoadingEnabled = false;
    }

    public DbSet<Product> Products { get; set; }
    public DbSet<Category> Categories { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder){

        modelBuilder.Configurations.Add(new ProductConfiguration());
        modelBuilder.Configurations.Add(new CategoriesConfiguration());
    }

    private class ProductConfiguration : EntityTypeConfiguration<Product> {
        public ProductConfiguration() {
            ToTable("Products");
            HasKey(p => p.ProductID);
            HasOptional(p => p.Category).WithMany(x=>x.Products).Map(c => c.MapKey("CategoryID"));
            Property(p => p.UnitPrice).HasColumnType("Money");
        }
    }

    private class CategoriesConfiguration : EntityTypeConfiguration<Category> {
        public CategoriesConfiguration() {
            ToTable("Categories");
            HasKey(p => p.CategoryID);
        }
    }
}

public class Category {
    public int CategoryID { get; set; }
    public string CategoryName { get; set; }
    public string Description { get; set; }
    public virtual ICollection<Product> Products { get; set; }
}

public class Product {
    public int ProductID { get; set; }
    public string ProductName { get; set; }
    public string QuantityPerUnit { get; set; }
    public decimal UnitPrice { get; set; }
    public Int16 UnitsInStock { get; set; }
    public Int16 UnitsOnOrder { get; set; }
    public Int16 ReorderLevel { get; set; }
    public bool Discontinued { get; set; }
    public virtual Category Category { get; set; }
}

The problem is that I can save anything from the Product but not the change of the category.

The object productFromForm contains the new CategoryID inside productFromForm.Product.ProductID without problem. But, when I Find() the category to retrieve the object from the context I have an object without Name and Description (both stay to NULL) and the SaveChanges() doesn't modify the reference even if the ID has changed for the property Category .

Any idea why?

Your (apparently) changed relationship doesn't get saved because you don't really change the relationship:

context.Products.Attach(productFromForm);

This line attaches productFromForm AND productFromForm.Category to the context.

var fromBD = context.Categories.Find(productFromForm.Category.CategoryID);

This line returns the attached object productFromForm.Category , NOT the object from the database.

productFromForm.Category = fromBD;

This line assigns the same object, so it does nothing.

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

This line only affects the scalar properties of productFromForm , not any navigation properties.

Better approach would be:

// 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();

It becomes a lot easier if you expose foreign keys as properties in the model - as already said in @Leniency's answer and in the answer to your previous question. With FK properties (and assuming that you bind Product.CategoryID directly to a view and not Product.Category.CategoryID ) the code above reduces to:

var fromBD = context.Products
    .Single(p => p.ProductId == productFromForm.ProductId);
context.Entry(fromBD).CurrentValues.SetValues(productFromForm);
context.SaveChanges();

Alternatively you can set the state to Modified which would work with FK properties:

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

The problem is that EF tracks association updates differently than value types. When you do this, context.Products.Attach(productFromForm); , the productFromForm is just a poco that doesn't track any changes. When you mark it as modified, EF will update all value types, but not associations.

A more common way is to do this:

[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();
}

The least-error prone solution I've found, especially as models get more complex, is two fold:

  • Use DTO's for any input (class ProductInput). Then use something like AutoMapper to map the data to your domain object. Especially useful as you start submitting increasingly complicated data.
  • Explicitly declare foreign keys in your domain objects. Ie, add a CategoryId do your product. Map your input to this property, not the association object. Ladislav's answer and subsequent post explain more on this. Both independent associations and foreign keys have their own issues, but so far I've found the foreign key method to have less headaches (ie, associated entities getting marked as added, order of attaching, crossing database concerns before mapping, etc...)

     public class Product { // EF will automatically assume FooId is the foreign key for Foo. // When mapping input, change this one, not the associated object. [Required] public int CategoryId { get; set; } public virtual Category Category { get; set; } } 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM