Entity Framework Core TrackGraph For Disconnected Data

Entity Framework Core is the new lightweight version of Entity Framework designed to work with .NET Core applications. Just like ASP.NET Core, it has been rewritten from the ground up, and includes some new ways of doing things. One of those is the introduction of the TrackGraph method for handling complex data in disconnected scenarios such as MVC or Web API applications.

Entity Framework is able to generate the correct SQL to be executed based on its understanding of the current state of any given entity graph. It knows about this because its ChangeTracker component keeps track of what happens to entities. And this works just fine in this kind of example:

using (var context = new TestContext())
{
    var author = context.Authors.Single(a => a.AuthorId == 1);
    author.FirstName = "Bill";
    context.SaveChanges();
}

The author entity is retrieved via the context which starts tracking it immediately. When the SaveChanges method is called, EF will detect any changes made to the entity it is tracking and generate SQL accordingly. In this example, only the FirstName property was changed:

exec sp_executesql N'SET NOCOUNT ON;
UPDATE[Authors] SET[FirstName] = @p0
WHERE[AuthorId] = @p1;
SELECT @@ROWCOUNT;
',N'@p1 int,@p0 nvarchar(4000)',@p1=1,@p0=N'Bill'

The situation becomes more complex when you want to work with entities that did not arise from the current instance of the context. They may arrive as a result of being posted to an MVC action method and then constructed by the model binder from form values. They may arrive having been deserialised from JSON in a Web API method. If you want the context to handle saving any changes, the entities need to be introduced to the context and the context needs to be informed about the action that is needed.

Entity Framework Core retains the DbContext Add and Attach methods from previous versions. It also includes a new Update method, and these methods work well for individual entities or in cases where you don't mind all properties being included in an UPDATE statement whether they were changed or not. Entity Framework Core also introduces a new method called TrackGraph, which enables you to navigate the entire object graph of a given entity (i.e. the root entity and all related entities, and all entities related to the related entities and so on, recursively), and to specify the action that should take place accordingly.

Here's an object graph (pretty simple for the purposes of example):

var author = new Author
{
    AuthorId = 1,
    FirstName = "William",
    LastName = "Shakespeare"
};
author.Books.Add(new Book { AuthorId = 1, BookId = 1, Title = "Hamlet", Isbn = "1234" });
author.Books.Add(new Book { AuthorId = 1, BookId = 2, Title = "Othello", Isbn = "4321" });
author.Books.Add(new Book { AuthorId = 1, BookId = 3, Title = "MacBeth", Isbn = "5678" });

Let's assume that the only thing that was changed was the ISBN numbers assigned to each book and that this object graph is presented to aSavemethod somewhere in a repository or service layer. The Save method makes use of the new DbContext.Update method:

public void Save(Author author)
{
    using(var context = new TestContext())
    {
        context.Update(author);
        context.SaveChanges();
    }
}

This works. The Update method will result in the root entity and all related entities being tracked as Modified, and SQL will be generated to update all of their properties to the values that have been assigned to the entities, whether they have been changed or not. That means that all of the values for all of the entities have to be present, otherwise they will be overwritten with null or default values.

The TrackGraph method offers finer-grained control since it provides easy access to each entity in the graph. Here's how the Save method might look instead:

public void Save(Author author)
{
    using (var context = new TestContext())
    {
        context.ChangeTracker.TrackGraph(author, e =>
            {
                e.Entry.State = EntityState.Unchanged;
                if ((e.Entry.Entity as Book) != null)
                {
                    context.Entry(e.Entry.Entity as Book).Property("Isbn").IsModified = true;
                }
            });
        context.SaveChanges();
    }
}

The TrackGraph method takes two arguments: the root entity to start tracking from, and a callback delegate to be performed on each entity that is discovered by recursion. In this example, the callback starts by applying an EntityState value to each entity so that it can be tracked. This is important for descendant discovery. If an entity is not assigned an EntityState value, it will not be tracked and its descendants will not be discovered. The UnChanged state is applied which will result in no action being taken by the context. However, if an entity is a Book type, its Isbn property is marked as modified, which results in the EntityState for that entity being changed to Modified. And then, just like in the example at the beginning of the article, SQL is generated that only updates the Isbn property:

exec sp_executesql N'SET NOCOUNT ON;
UPDATE[Books] SET[Isbn] = @p0
WHERE[BookId] = @p1;
SELECT @@ROWCOUNT;
UPDATE[Books] SET[Isbn] = @p2
WHERE[BookId] = @p3;
SELECT @@ROWCOUNT;
UPDATE[Books] SET[Isbn] = @p4
WHERE[BookId] = @p5;
SELECT @@ROWCOUNT;
',N'@p1 int,@p0 nvarchar(4000),@p3 int,@p2 nvarchar(4000),@p5 int,@p4 nvarchar(4000)',
@p1=1,@p0=N'1234',
@p3=2,@p2=N'4321',
@p5=3,@p4=N'5678'

Since it is possible to indicate which property was modified, there is no need to post values for other properties (apart from the key value, of course).

The body of the delegate that was passed in to the TrackGraph method could easily be factored out as a separate method to aid code re-usability:

void UpdateIsbn(EntityEntryGraphNode node, TestContext context)
{
    node.Entry.State = EntityState.Unchanged;
    if ((node.Entry.Entity as Book) != null)
    {
        context.Entry(node.Entry.Entity as Book).Property("Isbn").IsModified = true;
    }
}

Then it can be called like this:

public void Save(Author author)
{
    using (var context = new TestContext())
    {
        context.ChangeTracker.TrackGraph(author, e => UpdateIsbn(e, context));
        context.SaveChanges();
    }
}

Summary

The TrackGraph method is new in Entity Framework Core, and offers a simple way to iterate over a graph of objects that you want the context to begin tracking, and to apply customised code based on the type of entity and other criteria.

See more about Entity Framework Core at Learn Entity Framework Core.