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:
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
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/.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With