Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating many-to-many navigation property in Entity Framework 6, changes not being saved

I've been pulling my hair out for about 2 days now, as I simply cannot get EF to save changes whenever I add a many-to-many entity to an existing entity.

My structure is plain simple:

  • I have a table called Person, it has an ID (Primary, identity), and a few other string fields

  • A table called Keyword with an ID (Primary, identity) and a string field called Value

  • and a PersonKeywordRelation, with a PersonId, and a KeywordId field

When I have generated my entities (Database first), I get a class Person, with an ICollection<Keyword> - all good, works as expected.

The problem arises when I try to save an existing Person, with a modified list of keywords. Only the scalar properties (strings) are saved, not my keywords!

  • I've tried disabling Lazy Loading, no effect.
  • I tried loading each individual keyword from the database again, no effect.
  • I tried loading all keywords into the context to see if that would help EF detect changes, it didn't.

I am pretty sure I'm not the only one who have had this problem, (in fact I am entirely sure as I have seen a couple questions on here already, on the same subject, yet I am unable to find a working answer...), mostly for older versions of EF, which is another good reason as to why I started yet another question: Has nothing changed that addresses this issue at all?

Here is my code that does the updating (and creation) of persons. You'll notice my attempt on making EF save changes accordingly.

    public void SavePersons(IList<Person> persons)
    {
        // Create a EF Context
        using (var ctx = new MyDbEntities())
        {
            foreach (var person in persons)
            {
                // Attach
                ctx.Persons.Attach(person);

                // Insert or update?
                ctx.Entry(person).State = person.Id == 0 ? EntityState.Added : EntityState.Modified;

                // Get current keywords before clearing from entity
                var keywords = new List<Keyword>(person.Keywords);

                // Clear keywords from entity, so we can add fresh ones, hopefully
                // EF will have an easier time handling this..
                person.Keywords.Clear();

                // Add keywords
                keywords.ForEach(kw =>
                {
                    ctx.Keywords.Attach(kw);
                    ctx.Entry(kw).State = EntityState.Modified;
                    person.Keywords.Add(kw);
                });            
            }

            // Save
            ctx.SaveChanges();
        }
    }
like image 534
Jeff Avatar asked Jan 31 '13 17:01

Jeff


3 Answers

Finally.. Finally I can rest! I found the solution! It's not a pretty one, but it works!

Here's the code - sharing is caring.

This is definitely the last time I will work with Entity Framework. Causes more pain & agony than good.

    public void SavePersons(IList<Person> persons)
    {
        // Create a EF Context
        using (var ctx = new MyDbEntities())
        {
            // Iterate
            foreach (var person in persons)
            {
                // Get current keywords
                var keywords = new List<Keyword>(person.Keywords).ToList();

                // Fetch Person from DB (if its not a NEW entry). Must use Include, else it's not working.
                var newPerson = ctx.Persons
                                       .Include("Keywords")
                                       .FirstOrDefault(s => s.Id == person.Id) ?? person;

                // Clear keywords of the object, else EF will INSERT them.. Silly.
                newPerson.Keywords.Clear();

                // Insert or update?
                ctx.Entry(newPerson).State = newPerson.Id == 0 ? EntityState.Added : EntityState.Modified;

                // Apply new scalar values
                if(newPerson.Id != 0)
                {
                    person.Id = newPerson.Id;
                    ctx.Entry(newPerson).CurrentValues.SetValues(person);

                }

                // Iterate through all keywords
                foreach (var kw in ctx.Keywords)
                {
                    // If the current kw exists in OUR list, add it
                    // - if not, remove the relation from the DB.
                    if (keywords.Any(k => k.Id == kw.Id))
                    {
                        //ctx.Entry(kw).State = EntityState.Unchanged;
                        ctx.Keywords.Attach(kw);
                        newPerson.Keywords.Add(kw);
                    }
                    else
                        newPerson.Keywords.Remove(kw);
                }
            }

            // Save
            ctx.SaveChanges();

        }
    }
like image 91
Jeff Avatar answered Oct 23 '22 20:10

Jeff


Try adding .ToList():

var keywords = new List<Keyword>(person.Keywords).ToList();//generate list sepereate from .Keywords

I'm suspecting that your Keywords list is never populated because you clear it before hydrating it.

like image 35
Heather Avatar answered Oct 23 '22 18:10

Heather


So the following is untested but, after you fix my bugs ;) it should hopefully do the trick. i don't know the rest of your code so i've opted for creating clones of the input data and attaching the objects to the context in a specific order.

EDIT: renamed method

    // get unique list of Keywords from all Persons
    private List<Keyword> getUniqueKeywords(IEnumerable<Person> oxygenThiefs) {
        var result = new List<Keyword>();

        foreach (var thief in oxygenThiefs) {
            foreach (var keyword in thief.Keywords) {
                if (!result.Contains(keyword)) {
                    result.Add(keyword);
                }
            }
        }

        return result;
    }

    // shallow clone of Person
    private Person clonePerson(Person target) {

        return new Person {
            Id = target.Id,
            Name = target.Name,
            ..
            ..

        };
    }

    public void SavePersons(IList<Person> persons) {
        // Create a EF Context
        using (var ctx = new MyDbEntities()) {

            // add all Keywords to the Context so that they are tracked
            foreach (var keyword in getUniqueKeywords(persons)) {
                ctx.Keywords.Attach(keyword);
                // if value of Keyword has actually changed then uncomment line
                // ctx.Entry(keyword).State = EntityState.Modified
            }

            foreach (var person in persons) {

                // hehe
                var shallowPerson = clonePerson(person);

                // Attach Person
                ctx.Persons.Attach(shallowPerson);

                // Establish relationship (however shallow and meaningless)
                foreach (var keyword in person.Keywords) {
                    shallowPerson.Keywords.Add(keyword);
                }

                // Insert or update?
                ctx.Entry(shallowPerson).State = person.Id == 0 ? EntityState.Added : EntityState.Modified;

            }

            // Save
            ctx.SaveChanges();
        }
    }
like image 21
Quinton Bernhardt Avatar answered Oct 23 '22 18:10

Quinton Bernhardt