62

I am currently working on a project using the latest version of Entity Framework and I have come across an issue which I can not seem to solve.

When it comes to updating existing objects, I can fairly easily update the object properties ok, until it comes to a property which is a reference to another class.

In the below example I have a class called Foo, which stores various properties, with 2 of these being instances of other classes

public class Foo
{
     public int Id {get; set;}
     public string Name {get; set;}
     public SubFoo SubFoo {get; set}
     public AnotherSubFoo AnotherSubFoo {get; set}
}

When I use the below Edit() method, I pass in the object I wish to update and I can manage to get the Name to properly update, however I have not managed to find a way in which to get the properties of the SubFoo to change. For example, if the SubFoo class has a property of Name, and this has been changed and is different between my DB and the newFoo, it does not get updated.

public Foo Edit(Foo newFoo)
{
    var dbFoo = context.Foo
        .Include(x => x.SubFoo)
        .Include(x => x.AnotherSubFoo)
        .Single(c => c.Id == newFoo.Id);

    var entry = context.Entry<Foo>(dbFoo);
    entry.OriginalValues.SetValues(dbFoo);
    entry.CurrentValues.SetValues(newFoo);

    context.SaveChanges();

    return newFoo;
}

Any help or pointers would be greatly appreciated.

UPDATE: Based on the comment by Slauma I have modified my method to

public Foo Edit(Foo newFoo)
{
    var dbFoo = context.Foo
        .Include(x => x.SubFoo)
        .Include(x => x.AnotherSubFoo)
        .Single(c => c.Id == newFoo.Id);

    context.Entry(dbFoo).CurrentValues.SetValues(newFoo);
    context.Entry(dbFoo.SubFoo).CurrentValues.SetValues(newFoo.SubFoo);

    context.SaveChanges();

    return newFoo;
}

When running this now, I get the error:

The entity type Collection`1 is not part of the model for the current context.

To try and get around this, I added code to try to attach the newFoo subclasses to the context, but this through an error saying that the ObjectManager already had an entity the same:

An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key

Massimiliano Kraus
  • 3,638
  • 5
  • 27
  • 47
Thewads
  • 4,953
  • 11
  • 55
  • 71

3 Answers3

100

CurrentValues.SetValues only updates scalar properties but no related entities, so you must do the same for each related entity:

public Foo Edit(Foo newFoo)
{
    var dbFoo = context.Foo
                       .Include(x => x.SubFoo)
                       .Include(x => x.AnotherSubFoo)
                       .Single(c => c.Id == newFoo.Id);

    context.Entry(dbFoo).CurrentValues.SetValues(newFoo);
    context.Entry(dbFoo.SubFoo).CurrentValues.SetValues(newFoo.SubFoo);
    context.Entry(dbFoo.AnotherSubFoo).CurrentValues.SetValues(newFoo.AnotherSubFoo);

    context.SaveChanges();

    return newFoo;
}

If the relationship could have been removed altogether or have been created you also need to handle those cases explicitly:

public Foo Edit(Foo newFoo)
{
    var dbFoo = context.Foo
                       .Include(x => x.SubFoo)
                       .Include(x => x.AnotherSubFoo)
                       .Single(c => c.Id == newFoo.Id);

    context.Entry(dbFoo).CurrentValues.SetValues(newFoo);
    if (dbFoo.SubFoo != null)
    {
        if (newFoo.SubFoo != null)
        {
            if (dbFoo.SubFoo.Id == newFoo.SubFoo.Id)
                // no relationship change, only scalar prop.
                context.Entry(dbFoo.SubFoo).CurrentValues.SetValues(newFoo.SubFoo);
            else
            {
                // Relationship change
                // Attach assumes that newFoo.SubFoo is an existing entity
                context.SubFoos.Attach(newFoo.SubFoo);
                dbFoo.SubFoo = newFoo.SubFoo;
            }
        }
        else // relationship has been removed
            dbFoo.SubFoo = null;
    }
    else
    {
        if (newFoo.SubFoo != null) // relationship has been added
        {
            // Attach assumes that newFoo.SubFoo is an existing entity
            context.SubFoos.Attach(newFoo.SubFoo);
            dbFoo.SubFoo = newFoo.SubFoo;
        }
        // else -> old and new SubFoo is null -> nothing to do
    }

    // the same logic for AnotherSubFoo ...

    context.SaveChanges();

    return newFoo;
}

You eventually also need to set the state of the attached entities to Modified if the relationship has been changed and the scalar properties as well.

Edit

If - according to your comment - Foo.SubFoo is actually a collection and not only a reference you will need something like this to update the related entities:

public Foo Edit(Foo newFoo)
{
    var dbFoo = context.Foo
                       .Include(x => x.SubFoo)
                       .Include(x => x.AnotherSubFoo)
                       .Single(c => c.Id == newFoo.Id);

    // Update foo (works only for scalar properties)
    context.Entry(dbFoo).CurrentValues.SetValues(newFoo);

    // Delete subFoos from database that are not in the newFoo.SubFoo collection
    foreach (var dbSubFoo in dbFoo.SubFoo.ToList())
        if (!newFoo.SubFoo.Any(s => s.Id == dbSubFoo.Id))
            context.SubFoos.Remove(dbSubFoo);

    foreach (var newSubFoo in newFoo.SubFoo)
    {
        var dbSubFoo = dbFoo.SubFoo.SingleOrDefault(s => s.Id == newSubFoo.Id);
        if (dbSubFoo != null)
            // Update subFoos that are in the newFoo.SubFoo collection
            context.Entry(dbSubFoo).CurrentValues.SetValues(newSubFoo);
        else
            // Insert subFoos into the database that are not
            // in the dbFoo.subFoo collection
            dbFoo.SubFoo.Add(newSubFoo);
    }

    // and the same for AnotherSubFoo...

    db.SaveChanges();

    return newFoo;
}
Slauma
  • 175,098
  • 59
  • 401
  • 420
  • thanks for the comment, I have posted an update in my question – Thewads Nov 06 '12 at 09:04
  • @Thewads: I can only imagine that you get this "*Collection... is not part of the model...*" error when `Foo.SubFoo` is a *collection* type. But it is only a reference `public SubFoo SubFoo {get; set}` in your example model, not a collection. Is it really no collection? My answer was based on your current model where `SubFoo` is not a collection. – Slauma Nov 06 '12 at 10:29
  • Sorry, I did not realise that the model having collections would make a difference...my mistake. In the model I am dealing with: public ICollection SubFoo {get; set;} – Thewads Nov 06 '12 at 10:34
  • @Thewads: See my Edit section about updating a collection. – Slauma Nov 06 '12 at 12:31
  • Can you give some reference where I can find comprehensive guide how to tackle updating graphs with entity framework? What was your source except experience? I will be very grateful – VsMaX Dec 06 '13 at 19:39
  • @MichaelCwienczek: Sorry, but I really don't have any reference. – Slauma Dec 06 '13 at 22:18
  • 21
    I don't get why EF is such an overkill for a similar situation. There must be an easier way of doing such an update. – anar khalilov Jan 27 '14 at 08:20
  • 1
    When I implement this I got an error like that; "The property 'Id' is part of the object's key information and cannot be modified." – Serhat Koroglu Feb 26 '15 at 09:58
  • 1
    In Edit section we need to add one more condition of checking if id == 0 something like if (dbSubFoo != null && dbSubFoo.ID != 0) because if you have 5 new items then all will have id as 0 and once we add 1st one all later will be treated as existing because it will found item with id 0 as existing – Anshul Nigam Aug 11 '15 at 09:12
  • @Slauma, in Last Edit, context.SubFoos.Remove(dbSubFoo); & dbFoo.SubFoo.Add(newSubFoo);. Why remove from context and Add to dbFoo. Can't we do it with dbFoo.SubFoo.Remove(....) – Prasad Kanaparthi Sep 13 '15 at 04:58
  • 1
    @AnshulNigam, Good point :). For me dbSubFoo.Id != Guid.Empty – Prasad Kanaparthi Sep 13 '15 at 05:06
  • 2
    If checking for newly added id ==0, add "&& newSubFoo.Id != 0" to var dbSubFoo = dbFoo.SubFoo.SingleOrDefault(s => s.Id == newSubFoo.Id); as well to allow adding of more than 1 new record. – djrpascu Dec 17 '15 at 20:02
  • Thanks so much for this answer. It saved me a lot of time today. Unfortunately in 2021 we still need to do manual work like this in EF core – sean717 Mar 06 '21 at 02:20
  • For me when subFoo is collection, the SubFoo Entity is removed from the collection when "context.Entry(dbSubFoo).CurrentValues.SetValues(newSubFoo);" is executed. How can I prevent this? – Snaketec Apr 27 '22 at 14:07
3

Just thought I would post the link below as it really helped me understand how to update related entities.

Updating related data with the entity framework in an asp net mvc application

NOTE: I did change the logic slightly that is shown in the UpdateInstructorCourses function to suit my needs.

craigvl
  • 280
  • 2
  • 9
0

You can also call the tables independently

MyContext db = new MyContext
// I like using asynchronous calls in my API methods 
var OldFoo = await db.Foo.FindAsync(id);
var OldAssociateFoo = db.AssociatedFoo;  
var NewFoo = OldFoo;
var NewAssociatedFoo = OldAssociatedFoo;

NewFoo.SomeValue = "The Value";
NewAssociatedFoo.OtherValue = 20;

db.Entry(OldFoo).CurrentValues.SetValues(NewFoo);
db.Entry(OldAssociatedFoo).CurrentValues.SetValues(NewAssociatedFoo);

await db.SaveChangesAsync();
iuppiter
  • 315
  • 3
  • 9