Entity 4.1 Updating an existing parent entity with new child Entities
Asked Answered
G

4

27

I have an application where you can create a new type of product and add to that product some ingredients. The product and the ingredients are both entities saved in a database. The product entity has a collection of ingredient entities.

(simplified version)

public class Product
   Public Sub New()
     Me.Ingredients = New List(Of Ingredient)()
   End Sub

   Property Ingredients as ICollection(Of Ingredient)
end class

When I save the product for the first time, all goes well: I just add it to the context and call SaveChanges.

myDataContext.Products.Add(product)
myDataContext.SaveChanges()

Both the product (parent) and the ingredients (children) are saved and linked to each other. All is well.

However when I add/remove an ingredient to an existing product, I start running into problems. I first clear the existing ingredients collection in the product entity and then add the updated list of ingredients again (I don't re-use ingredients add the moment). I then change the state of the product entity to modified and call savechanges. On the state changing I, however, get the exception "An object with the same key already exists in the ObjectStateManager".

myDataContext.Entry(product).State = EntityState.Modified

After "some" searching I figured out that the problem is that all the ingredients have a primary key of 0 (as they aren't added yet) and when you change the state of the parent entity (product), all child entities (ingredients) are attached to the context with the key of 0, which causes the problem as the keys are no longer unique.

I have been searching for a solution but can't figure out how to solve this problem. I tried adding the ingredients to the context before changing the state, but then the link between the product and ingredients is missing... How do I update an existing parent entity with new, not yet added child entities?

I use Entity Framework 4.1 and Code First.

Hope you can help me!

Geronto answered 1/11, 2011 at 15:14 Comment(0)
S
38

I first clear the existing ingredients collection in the product entity and than add the updated list of ingredients again.

Well, this is kind of brute-force-attack to update the child collection. EF doesn't have any magic to update the children - which means: adding new children, deleting removed children, updating existing children - by only setting the state of the parent to Modified. Basically this procedure forces you to delete the old children also from the database and insert the new one, like so:

// product is the detached product with the detached new children collection
using (var context = new MyContext())
{
    var productInDb = context.Products.Include(p => p.Ingredients)
        .Single(p => p.Id == product.Id);

    // Update scalar/complex properties of parent
    context.Entry(productInDb).CurrentValues.SetValues(product);

    foreach (var ingredient in productInDb.Ingredients.ToList())
        context.Ingredients.Remove(ingredient);

    productInDb.Ingredients.Clear(); // not necessary probably

    foreach (var ingredient in product.Ingredients)
        productInDb.Ingredients.Add(ingredient);

    context.SaveChanges();
}

The better procedure is to update the children collection in memory without deleting all children in the database:

// product is the detached product with the detached new children collection
using (var context = new MyContext())
{
    var productInDb = context.Products.Include(p => p.Ingredients)
        .Single(p => p.Id == product.Id);

    // Update scalar/complex properties of parent
    context.Entry(productInDb).CurrentValues.SetValues(product);

    var ingredientsInDb = productInDb.Ingredients.ToList();
    foreach (var ingredientInDb in ingredientsInDb)
    {
        // Is the ingredient still there?
        var ingredient = product.Ingredients
            .SingleOrDefault(i => i.Id == ingredientInDb.Id);

        if (ingredient != null)
            // Yes: Update scalar/complex properties of child
            context.Entry(ingredientInDb).CurrentValues.SetValues(ingredient);
        else
            // No: Delete it
            context.Ingredients.Remove(ingredientInDb);
    }

    foreach (var ingredient in product.Ingredients)
    {
        // Is the child NOT in DB?
        if (!ingredientsInDb.Any(i => i.Id == ingredient.Id))
            // Yes: Add it as a new child
            productInDb.Ingredients.Add(ingredient);
    }

    context.SaveChanges();
}
Sensillum answered 1/11, 2011 at 16:12 Comment(5)
Took me long to find how to properly update entities. Thanks for context.Entry(productInDb).CurrentValues.SetValues(product);Felid
This is great, now I just need to figure out how to make it work via reflection so I can loop through all collections on an entity I am saving.Despondency
This makes me want to cry.Common
Why does this have to be so complicated?Carrie
I know it is a long time but something bothers me. By the use of method Entry I deduce that context id of type MyContext whice inherits from DbContext. Yet DbContetx does not have Ingredients. Actually none of the classes derived from DbContext have navigation properties. Am I missing something?Merrill
M
4

I found this recent article on the GraphDiff extension for DbContext.

Apparently it is a generic, reusable variant of Slauma's solution.

Example code:

using (var context = new TestDbContext())
{
    // Update DBcompany and the collection the company and state that the company 'owns' the collection Contacts.
    context.UpdateGraph(company, map => map.OwnedCollection(p => p.Contacts));     
    context.SaveChanges();
}

On a side note; I see the author has proposed to the EF team to use his code in issue #864 Provide better support for working with disconnected entities.

Manualmanubrium answered 24/3, 2013 at 23:7 Comment(0)
B
-1

I reckon, This is more simpler solution.

public Individual
{
.....

public List<Address> Addresses{get;set;}


}

//where base.Update from Generic Repository
public virtual void Update(T entity)
        {
            _dbset.Attach(entity);
            _dataContext.Entry(entity).State = EntityState.Modified;
        }

//overridden update
 public override void Update(Individual entity)
        {


            var entry = this.DataContext.Entry(entity);
            var key = Helper.GetPrimaryKey(entry);
            var dbEntry = this.DataContext.Set<Individual>().Find(key);

            if (entry.State == EntityState.Detached)
            {
                if (dbEntry != null)
                {
                    var attachedEntry = this.DataContext.Entry(dbEntry);
                    attachedEntry.CurrentValues.SetValues(entity);
                }
                else
                {
                    base.Update(entity);
                }
            }
            else
            {
                base.Update(entity);
            }
            if (entity.Addresses.Count > 0)
            {
                foreach (var address in entity.Addresses)
                {
                    if (address != null)
                    {
                        this.DataContext.Set<Address>().Attach(address);
                        DataContext.Entry(address).State = EntityState.Modified;
                    }
                }
            }
        }
Brimmer answered 28/6, 2013 at 15:26 Comment(0)
K
-2

after many many months struggling with understanding this entire crappy Entity Framework I hope this can help someone and not go through any of the frustration I have endured.

public void SaveOrder(SaleOrder order)
        {
            using (var ctx = new CompanyContext())
            {
                foreach (var orderDetail in order.SaleOrderDetails)
                {
                    if(orderDetail.SaleOrderDetailId == default(int))
                    {
                        orderDetail.SaleOrderId = order.SaleOrderId;
                        ctx.SaleOrderDetails.Add(orderDetail);
                    }else
                    {
                        ctx.Entry(orderDetail).State = EntityState.Modified;
                    }
                }

                ctx.Entry(order).State = order.SaleOrderId == default(int) ? EntityState.Added : EntityState.Modified;
                ctx.SaveChanges();                

            }

        }
Kaisership answered 4/1, 2013 at 9:47 Comment(1)
it seems to me that you didn't consider deleted orderDetailsGuberniya

© 2022 - 2024 — McMap. All rights reserved.