August 2016
Volume 31 Number 8
[Data Points]
EF Core Change-Tracking Behavior: Unchanged, Modified and Added
By Julie Lerman
Did you see what I did there, in this column’s title? You may have recognized Unchanged, Modified and Added as enums for EntityState in Entity Framework (EF). They also help me describe the behaviors of change tracking in EF Core compared to earlier versions of Entity Framework. Change tracking has become more consistent in EF Core so you can be more confident in knowing what to expect when you’re working with disconnected data.
Keep in mind that while EF Core attempts to keep the paradigms and much of the syntax of earlier versions of Entity Framework, EF Core is a new set of APIs—a completely new code base written from scratch. Therefore, it’s important not to presume that everything will behave exactly as it did in the past. The change tracker is a critical case in point.
Because the first iteration of EF Core is targeted to align with ASP.NET Core, a lot of the work focused on disconnected state, that is, making sure Entity Framework can handle the state of objects coming out of band, so to speak, where EF hasn’t been keeping track of those objects. A typical scenario is a Web API that’s accepting objects from a client application to be persisted to the database.
In my March 2016 Data Points column, I wrote about “Handling the State of Disconnected Entities in EF” (msdn.com/magazine/mt694083). That article focused on assigning state information to the disconnected entities and sharing that information with EF when passing those objects back into EF’s change tracker. Though I used EF6 to lay out the example, that pattern is still relevant for EF Core, so after discussing the EF Core behaviors, I’ll show an example of how I’d implement that pattern in EF Core.
Tracking with DbSet: Modified
DbSet always included the Add, Attach and Remove methods. The result of these methods on a single object are simple enough, they set the state of the object to the relevant EntityState. Add results in Added, Attach in Unchanged and Remove changes the state to Deleted. There’s one exception, which is that if you remove an entity that’s already known as Added, it will be detached from the DbContext because there’s no longer a need to track the new entity. In EF6, when you use these methods with graphs, the effects on the related objects were not quite as consistent. A formerly untracked object couldn’t be removed and would throw an error. Already tracked objects may or may not have their state altered, depending on what those states are. I created a set of tests in EF6 in order to comprehend the various behaviors, which you can find on GitHub at bit.ly/28YvwYd.
While creating EF Core, the EF team experimented with the behavior of these methods throughout the betas. In EF Core RTM, the methods no longer behave as they did in EF6 and earlier. For the most part, the changes to these methods result in more consistent behavior on which you can rely. But it’s important to understand how they’ve changed.
When you use Add, Attach and Remove with an object that has a graph attached, the state of every object in the graph that’s unknown to the change tracker will be set to the state identified by the method. Let me clarify this using my favorite EF Core model from the “Seven Samurai” film—samurais with movie quotes attached, and other related information.
If a samurai is new and not being tracked, Samurais.Add will set that samurai’s state to Added. If the samurai has a quote attached to it when Add is called, its state will also be set to Added. This is desired behavior and, in fact, is the same as it was in EF6.
What if you’re adding a new quote to an existing samurai and, rather than following my recommendation to set newQuote.SamuraiId to the value of Samurai.Id, you instead set the navigation property, newQuote.Samurai=oldSamurai. In a disconnected scenario, where neither the quote nor the oldSamurai is being tracked by EF, Quotes.Add(newQuote) will do the same as the preceding. It will mark the newQuote as Added and the oldSamurai as Added. SaveChanges will insert both objects into the database and you’ll have a duplicate oldSamurai in the database.
If you’re in a client application, for example, Windows Presentation Foundation (WPF), and you use your context to query for the samurais and then use that same context instance to call context.Quotes.Add(newQuote), the context already knows about the oldSamurai and won’t change its Unchanged state to Added. That’s what I mean by not changing the state of already tracked objects.
The way the change tracker affects related objects in a disconnected graph is notably different and you should keep these differences in mind when using these methods in EF Core.
Rowan Miller summarized the new behavior in a GitHub issue (bit.ly/295goxw):
Add: Adds every reachable entity that isn’t already tracked.
Attach: Attaches every reachable entity, except where a reachable entity has a store-generated key and no key value is assigned; these will be marked as added.
Update: Same as Attach, but entities are marked as modified.
Remove: Same as Attach, and then mark the root as deleted. Because cascade delete now happens on SaveChanges, this allows cascade rules to flow to entities later on.
There’s one more change to the DbSet methods that you might notice in this list: DbSet finally has an Update method, which will set the state of untracked objects to Modified. Hooray! What a nice alternative to always having to add or attach and then explicitly set the state to Modified.
DbSet Range Methods: Also Modified
Two range methods on DbSet (AddRange and RemoveRange) were introduced in EF6 and allow you to pass in an array of like types. This provided a big performance boost because the change tracker engages only once, rather than on each element of the array. The methods do call Add and Remove as detailed earlier and, therefore, you need to consider how related graph objects are affected.
In EF6, the range methods existed only for Add and Remove, but EF Core now brings UpdateRange and AttachRange. The Update and Attach methods that are called individually for each object or graph passed into the Range methods will behave as described earlier.
DbContext Change Tracking Methods: Added
If you worked with EF ObjectContext prior to the introduction of the DbContext, you might recall that ObjectContext had Add, Attach and Delete methods. Because the context had no way of knowing to which ObjectSet the target entity belonged, you had to add a string representation of the ObjectSet name as a parameter. This was so messy and most of us found it easier just to use the ObjectSet Add, Attach and Delete methods. When DbContext came along, those messy methods went away and you could only Add, Attach and Remove via the DbSet.
In EF Core, the Add, Attach and Remove methods are back as methods on the DbContext, along with Update and the four related Range methods (AddRange, and so forth). But these methods are much smarter now. They’re now able to determine the type and automatically relate the entity to the correct DbSet. This is really convenient because it allows you to write generic code without having to instantiate a DbSet. The code is simpler and, more important, more discoverable. Here’s a comparison of EF6 and EF Core:
private void AddToSetEF6<T>(T entity) where T : class {Pull using (var context = new SamuraiContext()) { context.Set<T>().Add(entity); } } private void AddToSetEFCore(object entity) { using (var context = new SamuraiContext()) { context.Add(entity); } }
The range methods are even more helpful because you can pass in a variety of types and EF can work them out:
private void AddViaContextEFCore(object[] entitiesOfVaryingTypes) { using (var context = new SamuraiContext()) { context.AddRange(entitiesOfVaryingTypes); } }
DbContext.Entry: Modified—Beware This Change in Behavior
Even though we’ve been warned that EF Core is not EF6 and that we should not expect familiar code to behave as it did in EF6, it’s still difficult not to have such an expectation when so many behaviors have carried forward. DbContext.Entry is a case in point, though, and it’s important you understand how it has changed.
The change is a welcome one to me because it brings consistency to change tracking. In EF6, the DbSet Add (and so forth) methods and the DbContext.Entry method combined with the State property had the same impact on entities and on graphs. So using DbContext.Entry(object).State=EntityState.Added would make all of the objects in a graph (that were not already being tracked) Added.
Moreover, there was never an intuitive way to disconnect graph objects before passing them to the change tracker.
In EF Core, DbContext.Entry now affects only the object being passed in. If that object has other related objects connected to it, DbContext.Entry will ignore them.
If you’re used to using the Entry method to connect graphs to a DbContext instance, you can see why this change is drastic. It means you can target an individual object even if it’s part of a graph.
More important, you can now explicitly use the DbSet and DbContext tracking methods (Add, and the like) to work explicitly with graphs, and you can use the DbContext.Entry method to work specifically with individual objects. This, combined with the next change I explain, means you now have clear options to select from when passing object graphs into the EF Core change tracker.
DbContext.ChangeTracker.TrackGraph: Added
TrackGraph is a completely new concept in EF Core. It provides ultimate control for each object in an object graph that you want your DbContext to begin tracking.
TrackGraph walks the graph (that is, it iterates through each object in the graph) and applies a designated function to each of those objects. The function is the second parameter of the TrackGraph method.
The most common example is one that sets the state of each object to a common state. In the following code, TrackGraph will iterate through all of the objects in the newSword graph and set their state to Added:
context.ChangeTracker.TrackGraph(newSword, e => e.Entry.State = EntityState.Added);
The same caveat as the DbSet and DbContext methods applies to TrackGraph—if the entity is already being tracked, TrackGraph will ignore it. While this particular use of TrackGraph behaves the same as the DbSet and DbContext tracking methods, it does provide more opportunity for writing reusable code:
The lambda (“e” in this code) represents an EntityEntryGraphNode type. The EntityEntryGraphNode type also exposes a property called NodeType and you might encounter it via IntelliSense as you type the lambda. This seems to be for internal use and won’t have the effect e.Entry.State provides, so be careful not to use it unwittingly.
In a disconnected scenario, the caveat about already tracked objects being ignored may not be relevant. That’s because the DbContext instance will be new and empty, so all of the graph objects should be new to the DbContext. However, consider the possibility of passing a collection of graphs into a Web API. Now there’s a chance of multiple references to a common object and EF’s change tracker will check identity to determine if an entity is already being tracked. That’s a good case for it not to add the object to the change tracker a second time.
This default behavior is designed to cover the most common scenarios. But I can imagine that, like me, you may already be thinking of edge cases where this pattern might fail.
This is where I’ll hearken back to my March 2016 article and the pattern I shared for setting the object state on your classes and then reading that state to tell the change tracker what the object’s EntityState should be. Now I can combine that pattern and the TrackGraph method by having the function TrackGraph calls perform the task of setting the EntityState based on the object’s State method.
The work on the domain classes doesn’t change from what I did in the March article. I start by defining an enum for the locally tracked ObjectState:
public enum ObjectState { Unchanged, Added, Modified, Deleted }
Then I build an IEntityObjectWithState interface that exposes a State property based on the enum:
public interface IObjectWithState { ObjectState State { get; set; } }
Now, I fix up my classes to implement the interface. As an example, here’s a small class from Location, with the interface in place:
using SamuraiTracker.Domain.Enums; using SamuraiTracker.Domain.Interfaces; namespace SamuraiTracker.Domain { public class Location : IObjectWithState { public int Id { get; set; } public string Name { get; set; } public ObjectState State { get; set; } } }
In the March article, I showed how to build intelligent classes that are able to manage their local state. I haven’t repeated that in this example, which means that in my sample I’ve left the setter public and will have to manually set that state. In a fleshed out solution, I’d tighten up these classes to be more like those in the earlier article.
For the DbContext, I have some static methods in a helper class called ChangeTrackerHelpers, shown in Figure 1.
Figure 1 The ChangeTrackerHelpers Class
public static class ChangeTrackerHelpers { public static void ConvertStateOfNode(EntityEntryGraphNode node) { IObjectWithState entity = (IObjectWithState)node.Entry.Entity; node.Entry.State = ConvertToEFState(entity.State); } private static EntityState ConvertToEFState(ObjectState objectState) { EntityState efState = EntityState.Unchanged; switch (objectState) { case ObjectState.Added: efState = EntityState.Added; break; case ObjectState.Modified: efState = EntityState.Modified; break; case ObjectState.Deleted: efState = EntityState.Deleted; break; case ObjectState.Unchanged: efState = EntityState.Unchanged; break; } return efState; } }
ConvertStateOfNode is the method TrackGraph will call. It will set the EntityState of the object to a value determined by the ConvertToEFState method, which transforms the IObjectWithState.State value into an EntityState value.
With this in place, I can now use TrackGraph to begin tracking my objects along with their correctly assigned EntityStates. Here’s an example where I pass in an object graph called samurai, which consists of a Samurai with a related Quote and Sword:
context.ChangeTracker.TrackGraph(samurai, ChangeTrackerHelpers.ConvertStateOfNode);
In the EF6 solution, I had to add the items to the change tracker and then explicitly call a method that would iterate through all of the entries in the change tracker to set the relevant state of each object. The EF Core solution is much more efficient. Note that I haven’t yet explored possible performance impact when dealing with large amounts of data in a single transaction.
If you download the sample code for this column, you’ll see me using this new pattern within an integration test named CanApplyStateViaChangeTracker in which I create this graph, assign various states to the different objects and then verify that the resulting EntityState values are correct.
IsKeySet: Added
The EntityEntry object, which holds the tracking information for each entity, has a new property called IsKeySet. IsKeySet is a great addition to the API. It checks to see if the key property in the entity has a value. This eliminates the guessing game (and related code) to see if an object already has a value in its key property (or properties if you have a composed key). IsKeySet checks to see if the value is the default value of the particular type you specified for the key property. So if it’s an int, is it 0? If it’s a Guid, is it equal to Guid.Empty (00000000-0000-0000-0000-000000000000)? If the value is not the default for the type, IsKeySet returns true.
If you know that in your system you can unequivocally differentiate a new object from a pre-existing object by the value of its key property, then IsKeySet is a really handy property for determining the state of entities.
EF Core with Eyes Wide Open
While the EF team has certainly done what they can to ease the pain of transitioning your brain from earlier versions of Entity Framework to EF Core, replicating plenty of the syntax and behavior, it’s so important to keep in mind that these are different APIs. Porting code will be tricky and isn’t recommended—especially in these early days when the RTM has only a subset of familiar features. But even as you embark on new projects with confidence that the feature set in EF Core has what you need, don’t presume things will work the same. I still have to remind myself about this. Nevertheless, I’m pleased with the changes to the ChangeTracker. They bring more clarity, more consistency and more control for dealing with disconnected data.
The EF team has a roadmap on the GitHub page for which I created a convenient shortcut: bit.ly/efcoreroadmap. This lets you keep track of the features, though it won’t list the minutia of things like behavior changes. For that I recommend tests, lots of tests, to ensure things are working the way you expect. And if you’re planning to port code from earlier versions of EF, you may want to look into Llewellyn Falco’s Approval Tests (approvaltests.com), which let you compare output from tests to ensure that the outputs continue to match.: Erik Ejlskov Jensen
Erik Ejlskov Jensen is a .NET and database developer at NNIT A/S, and a Microsoft Data Platform MVP. He offers a number of free, open source tools and libraries for .NET database developers on GitHub, including the popular Visual Studio extension "SQLite & SQL Server Compact Toolbox". He has also created a database provider for Entity Framework Core. Follow him on Twitter: @ErikEJ for the latest news on .NET data access development.
Discuss this article in the MSDN Magazine forum