17

There's a certain amount of background to get through for this one - please bear with me!

We have a n-tier WPF application using EF - we load the data from the database via dbContext into POCO classes. The dbContext is destroyed and the user is then able to edit the data. We use "state painting" as suggested by Julie Lerman in her book "Programming Entity Framework: DBContext" so that when we add the root entity to a new dbContext for saving we can set whether each child entity is added, modified or left unchanged etc.

The problem we had when we first did this (back in November 2012!) was that if the root entity we are adding to the dbContext has multiple instances of the same child entity (ie, a "Task" record linked to a user, with "Status Histories" also linked to the same user) the process would fail because even though the child entities were the same (from the same database row) they were given different hashcodes so EF recognised them as different objects.

We fixed this, (back in December 2012!), by overriding GetHashCode on our entities to return either the database ID if the entity came from the database, or an unique negative number if the entity is as yet unsaved. Now when we add the root entity to the dbContext it was clever enough to realise the same child entity is being added more than once and it dealt with it correctly. This has been working fine since December 2012 until we upgraded to EF6 last week...

One of the new "features" with EF6 is that it now uses it's own Equals and GetHashCode methods to perform change-tracking tasks, ignoring any custom overrides. See: http://msdn.microsoft.com/en-us/magazine/dn532202.aspx (search for "Less Interference with your coding style"). This is great if you expect EF to manage the change-tracking but in a disconnected n-tier application we don't want this and in fact this breaks our code that has been working fine for over a year.

Hopefully this makes sense.

Now - the question - does anyone know of any way we can tell EF6 to use OUR GetHashCode and Equals methods like it did in EF5, or does anyone have a better way to deal with adding a root entity to a dbContext that has duplicated child entities in it so that EF6 would be happy with it?

Thanks for any help. Sorry for the long post.

UPDATED Having poked around in the EF code it looks like the hashcode of an InternalEntityEntry (dbEntityEntry) used to be set by getting the hashcode of the entity, but now in EF6 is retrieved by using RuntimeHelpers.GetHashCode(_entity), which means our overridden hashcode on the entity is ignored. So I guess getting EF6 to use our hashcode is out of the question, so maybe I need to concentrate on how to add an entity to the context that potentially has duplicated child entities without upsetting EF. Any suggestions?

UPDATE 2 The most annoying thing is that this change in functionality is being reported as a good thing, and not, as I see it, a breaking change! Surely if you have disconnected entities, and you've loaded them with .AsNoTracking() for performance (and because we know we are going to disconnect them so why bother tracking them) then there is no reason for dbContext to override our getHashcode method!

UPDATE 3 Thanks for all the comments and suggestion - really appreciated! After some experiments it does appear to be related to .AsNoTracking(). If you load the data with .AsNoTracking() duplicate child entities are separate objects in memory (with different hashcodes) so there is a problem state painting and saving them later. We fixed this problem earlier by overriding the hashcodes, so when the entities are added back to the saving context the duplicate entities are recognised as the same object and are only added once, but we can no longer do this with EF6. So now I need to investigate further why we used .AsNoTracking() in the first place. One other thought I have is that maybe EF6's change tracker should only use its own hashcode generation method for entries it is actively tracking - if the entities have been loaded with .AsNoTracking() maybe it should instead use the hashcode from the underlying entity?

UPDATE 4 So now we've ascertained we can't continue to use our approach (overridden hashcodes and .AsNoTracking) in EF6, how should we manage updates to disconnected entities? I've created this simple example with blogposts/comments/authors: Diagram and Data

In this sample, I want to open blogpost 1, change the content and the author, and save again. I've tried 3 approaches with EF6 and I can't get it to work:

BlogPost blogpost;

using (TestEntities te = new TestEntities())
{
    te.Configuration.ProxyCreationEnabled = false;
    te.Configuration.LazyLoadingEnabled = false;

    //retrieve blog post 1, with all comments and authors
    //(so we can display the entire record on the UI while we are disconnected)
    blogpost = te.BlogPosts
        .Include(i => i.Comments.Select(j => j.Author)) 
        .SingleOrDefault(i => i.ID == 1);
}

//change the content
blogpost.Content = "New content " + DateTime.Now.ToString("HH:mm:ss");

//also want to change the author from Fred (2) to John (1)

//attempt 1 - try changing ID? - doesn't work (change is ignored)
//blogpost.AuthorID = 1;

//attempt 2 - try loading the author from the database? - doesn't work (Multiplicity constraint violated error on Author)
//using (TestEntities te = new TestEntities())
//{
//    te.Configuration.ProxyCreationEnabled = false;
//    te.Configuration.LazyLoadingEnabled = false;
//    blogpost.AuthorID = 1;
//    blogpost.Author = te.Authors.SingleOrDefault(i => i.ID == 1);
//}

//attempt 3 - try selecting the author already linked to the blogpost comment? - doesn't work (key values conflict during state painting)
//blogpost.Author = blogpost.Comments.First(i => i.AuthorID == 1).Author;
//blogpost.AuthorID = 1;


//attempt to save
using (TestEntities te = new TestEntities())
{
    te.Configuration.ProxyCreationEnabled = false;
    te.Configuration.LazyLoadingEnabled = false;
    te.Set<BlogPost>().Add(blogpost); // <-- (2) multiplicity error thrown here

    //paint the state ("unchanged" for everything except the blogpost which should be "modified")
    foreach (var entry in te.ChangeTracker.Entries())
    {
        if (entry.Entity is BlogPost)
            entry.State = EntityState.Modified;
        else
            entry.State = EntityState.Unchanged;  // <-- (3) key conflict error thrown here
    }

    //finished state painting, save changes
    te.SaveChanges();

}

If you use this code in EF5, using our existing approach of adding .AsNoTracking() to the original query..

        blogpost = te.BlogPosts
        .AsNoTracking()
        .Include(i => i.Comments.Select(j => j.Author)) 
        .SingleOrDefault(i => i.ID == 1);

..and overriding GetHashCode and Equals on the entities: (for example, in the BlogPost entity)..

    public override int GetHashCode()
    {
        return this.ID;
    }

    public override bool Equals(object obj)
    {
        BlogPost tmp = obj as BlogPost;
        if (tmp == null) return false;
        return this.GetHashCode() == tmp.GetHashCode();
    }

..all three approaches in the code now work fine.

Please can you tell me how to achieve this in EF6? Thanks

Buzby
  • 573
  • 5
  • 13
  • Could you override it in a extension method to the data context objects, or modify the code generation template to include your override? – BradleyDotNET Jan 28 '14 at 17:26
  • Will be hard, if possible at all. I think you're going to have to keep track of hashcodes yourself during the `ApplyChanges` method: skip an object if its hashcode already passed. – Gert Arnold Jan 28 '14 at 21:59
  • LordTakkera - We have altered the standard code generation template to override GetHashCode and Equals on all of our entities. Gert Arnold - The entities have our hashcodes until we add them to the dbContext. But whereas previously EF would use our hashcodes to determine if two entities in the object graph were the same and only add them once, it now ignores the hashcodes and adds the "same" entity twice with two different hash codes. I may have a go at filtering the entities in the object graph to ensure only distinct objects are added to the dbContext. I'll report back.. – Buzby Jan 29 '14 at 09:18
  • The answer to this post maybe interesting to you: http://stackoverflow.com/q/21421008/861716 – Gert Arnold Jan 29 '14 at 12:52
  • Shouldn't you have some sort of identity map and one (and only one) instance per object? – ta.speot.is Jan 29 '14 at 12:52
  • In the specs, the is a workaround wrt children in the aggregate. So first thing is just to check to see if that's related and possibly helpful. http://entityframework.codeplex.com/wikipage?title=Support%20of%20POCO%20entities%20with%20custom%20Equals%20and%20GetHashCode%20implementations. Yeah..I guess breaking change if you already had a workaround. So if that's no good, let's ping Rowan or Arthur or Diego from the team. – Julie Lerman Jan 29 '14 at 13:17
  • @JulieLerman I've tried using the EqualityComparer suggested in the link you posted and still have the same problem. How do the EF team suggest we should re-attach an entity that has duplicate child entities in the object graph, if the overridden GetHashCode and Equals based on the database ID are now ignored? Maybe EF should have a configuration option that allows you to use your own hashcodes etc if necessary? – Buzby Jan 29 '14 at 16:45
  • @ta.speot.is - I've been puzzling over how to add the root entity and a distinct list of it's child entities (based on database IDs) without inadvertantly adding duplicate child entities. I have a method we use for debugging that lists all the entities in the object graph but how do I attach the appropriate entities to the context without adding the child entities? – Buzby Jan 29 '14 at 16:51
  • I did send a twitter note to a few guys on the team to check out this thread. I can't help wondering i fyou are having conflicts by ADDing objects that are modified or unchanged. Often it's easier to just add everyting and then fix the state. But if you have dupes. Maybe you need a different pattern that is appropriate. You're original workaround might not even be necessary. If you add something, EF ignores keys & does an insert. If you add 2 instances of same thing & change both to modified, then I'm not sure what to expect..wld have to experiment. Not sure exactly your scenario – Julie Lerman Jan 30 '14 at 21:15
  • Thanks - we add the root object to the context and then work through every entity in the change tracker applying state (added/modified/unchanged/deleted) as appropriate. The problem occurs if two items in the graph are the same (ie originally from the same database row) - if you leave the default EF hashcode implmentation it adds them both in "added" state but then when you try and change state to something else (even "unchanged") it (correctly) won't let you have two entities from the same table with the same ID. .. – Buzby Jan 31 '14 at 08:52
  • ...We fixed this by using the db ID as the hashcode - it then recognises that they are the same object during the Add method and only one copy would actually be added to the context. We can then state paint and save and it all works fine. However, now EF6 overrides our hashcode implementation we're back to the original problem. – Buzby Jan 31 '14 at 08:53
  • It is hard to find this post unless you already figured out what the problem is with EF6 :). SO, Is there a way to add an object graph to EF6??? Upgrading from EF4. I think it should honor our GetHashCode() overrides since that is what it is for!! I'm stumped on how to move forward on this. We start with DTOs which have a state and a list of modified properties so it should be straightforward, your would think.... – Jon B Dec 03 '20 at 20:39

1 Answers1

3

It’s interesting and surprising that you got your application working this way in EF5. EF always requires only a single instance of any entity. If a graph of objects are added and EF incorrectly assumes that it is already tracking an object when it is in fact tracking a different instance, then the internal state that EF is tracking will be inconsistent. For example, the graph just uses .NET references and collections, so the graph will still have multiple instances, but EF will only be tracking one instance. This means that changes to properties of an entity may not be detected correctly and fixup between instances may also result in unexpected behavior. It would be interesting to know if your code solved these problems in some way or if it just so happened that your app didn’t hit any of these issues and hence the invalid state tracking didn’t matter for your app.

The change we made for EF6 makes it less likely that an app can get EF state tracking into an invalid state which would then cause unexpected behavior. If you have a clever pattern to ensure the tracking state is valid that we broke with EF6 then it would be great if you could file a bug with a full repro at http://entityframework.codeplex.com/.

Arthur Vickers
  • 7,503
  • 32
  • 26
  • Thanks for your answer – we believed when we were adding the entity using the .Add() method, EF was automatically dealing with two entities with the same hashcode, but it sounds like from your answer that it was in fact ignoring and not tracking changes in the second one. I think our approach (by overriding the hashcode with the database ID) still worked for us because when there were multiple instances of the same entity in the object graph they were generally “lookup” type items and therefore were state painted as “unchanged” before the save... – Buzby Feb 04 '14 at 11:32
  • ..My question - what is the best practice for updating and saving disconnected entities in EF6? Our approach worked for us in EF5, but now with EF6 it doesn’t. See some sample code in UPDATE 4 above.. Thanks again. – Buzby Feb 04 '14 at 11:32
  • @Buzby This post was written by Diego on the EF team some time ago: http://blogs.msdn.com/b/diego/archive/2010/10/06/self-tracking-entities-applychanges-and-duplicate-entities.aspx. It talks about self tracking entities, but the general approach also applies in this situation. I think the most important point in this post is to avoid creating duplicates in the first place if at all possible. If this is not possible, then the post shows some ways to resolve duplicates, but this is in general a hard problem to solve. – Arthur Vickers Feb 06 '14 at 17:36