O'Reilly logo

Programming Entity Framework: DbContext by Rowan Miller, Julia Lerman

Stay ahead with the world's most comprehensive technology and business learning platform.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, tutorials, and more.

Start Free Trial

No credit card required

Tracking Individually Modified Properties

So far, the methods you’ve seen have focused on changing the state of an entity. For a lot of applications, it’s enough to simply track the state at the entity level and update entire objects just as you would when relying on stored procedures. However, you may find yourself wanting to be more granular with the way modified properties are tracked. Rather than marking the entire entity as modified, you might want only the properties that have actually changed to be marked as modified. This is how change tracking works when properties of a tracked entity are modified. It ensures that only the properties that have actually been changed would be included in the update statement. In this section we’ll take a quick look at some common approaches to achieving granular property tracking when the entities are disconnected from the context:

  • Keeping track of modified properties on the client side and passing that list to the server along with the entities

  • Storing the original properties into the entities when they are retrieved from the database before passing them onto the client

  • Requerying the database when the entities have been returned to the server from the client

Note

The samples provided in this section will provide you with a closer look at the Change Tracker API that is tied to the DbContext API. The Entity Framework team worked hard to make our lives easier when dealing with disconnected scenarios. You’ve already seen some of the benefits we developers can reap from their work, and you’ll see more as you read through to the end of this chapter.

Recording Modified Property Names

This first approach is very similar to tracking state at the entity level. In addition to marking an entity as modified, the client is also responsible for recording which properties have been modified. One way to do this would be to add a list of modified property names to the state tracking interface. Update the IObjectWithState interface to include a ModifiedProperties property, as shown in Example 4-20.

Example 4-20. State tracking interface updated to include modified properties

using System.Collections.Generic;

namespace Model
{
  public interface IObjectWithState
  {
    State State { get; set; }
    List<string> ModifiedProperties { get; set; }
  }

  public enum State
  {
    Added,
    Unchanged,
    Modified,
    Deleted
  }
}

You’ll also need to add this property to the Destination and Lodging classes to satisfy this new addition to the IObjectWithState interface:

public List<string> ModifiedProperties { get; set; }

Now that we have a place to record the modified properties, let’s update the ApplyChanges method to make use of it, as shown in Example 4-21.

Example 4-21. Updating ApplyChanges to use ModifiedProperties

private static void ApplyChanges<TEntity>(TEntity root)
  where TEntity : class, IObjectWithState
{
  using (var context = new BreakAwayContext())
  {
    context.Set<TEntity>().Add(root);

    CheckForEntitiesWithoutStateInterface(context);

    foreach (var entry in context.ChangeTracker
      .Entries<IObjectWithState>())
    {
      IObjectWithState stateInfo = entry.Entity;
      if (stateInfo.State  == State.Modified)
      {
        entry.State = EntityState.Unchanged;
        foreach (var property in stateInfo.ModifiedProperties)
        {
          entry.Property(property).IsModified = true;
        }
      }
      else
      {
        entry.State = ConvertState(stateInfo.State);
      }
    }

    context.SaveChanges();
  }
}

The changes to the method are inside the foreach loop where we apply the state. If we find a modified entity in the graph, we mark it as Unchanged, rather than Modified. Once the entity is marked as Unchanged, we loop through the modified properties and mark each of them as modified. We do this using the Property method on the entry to get the change tracking information for the given property and then setting the IsModified property to true. As soon as we mark one of the properties as modified, the state of the entity will also move to Modified. But only the properties we marked as modified will be updated when we save.

Note

There are two overloads of Property, one that accepts the name of the property as a string and the other that accepts a lambda expression representing the property. We are using the string property because we don’t know the names of the properties we want to access until runtime. If you know the name of the property you want to access, you can use the lambda version so that you get compile-time checking of the supplied value (for example, context.Entry(destination).Property(d => d.Name).IsModified = true).

You'll also need to update the TestSaveDestinationGraph method to populate ModifiedProperties for the Destination and Lodging that are modified:

canyon.TravelWarnings = "Carry enough water!";
      canyon.State = State.Modified;
      canyon.ModifiedProperties = new List<string> { "TravelWarnings" };

      var firstLodging = canyon.Lodgings.First();
      firstLodging.Name = "New Name Holiday Park";
      firstLodging.State = State.Modified;
      firstLodging.ModifiedProperties = new List<string> { "Name" };

If you run the application again, you will see a set of SQL statements similar to the ones in Figure 4-3. This time, however, the update statements only set the properties we marked as modified. Here is the SQL from the update statement for the Destination:

exec sp_executesql N'update [baga].[Locations]
set [TravelWarnings] = @0
where ([LocationID] = @1)
',N'@0 nvarchar(max) ,@1 int',
@0=N'Carry enough water!',@1=1

Concurrency implications

This approach has the same implications for concurrency as the generic approach you saw in the previous section. Timestamp concurrency properties will work well, but concurrency properties that can be modified on the client will cause issues.

Recording Original Values

An alternative to asking the client to record the properties that were modified is to keep track of the original values for existing entities. One of the big advantages of this approach is that you are no longer relying on the client to tell you which properties were modified. This makes the code on the client side much simpler and less error prone. Entity Framework can then check for changes between the original and current values to determine if anything is modified. Let’s change the IObjectWithState interface to record original values rather than modified properties. Because we are going to calculate whether an entity is modified or not, we no longer need the Modified option in the State enum, so let’s remove that, too. These changes are shown in Example 4-22.

Example 4-22. Change state tracking interface to use original values

using System.Collections.Generic;

namespace Model
{
  public interface IObjectWithState
  {
    State State { get; set; }
    Dictionary<string, object> OriginalValues { get; set; }
  }

  public enum State
  {
    Added,
    Unchanged,
    Deleted
  }
}

You’ll also need to remove the ModifiedProperties property from Destination and Lodging and add in the new OriginalValues property:

public Dictionary<string, object> OriginalValues { get; set; }

We want Entity Framework to automatically populate the OriginalValues property when an entity is retrieved from the database, so let’s update the event handler we added to the constructor of our context (Example 4-23). You’ll need to add a using statement for the System.Collections.Generic namespace.

Example 4-23. Populating original values after query

public BreakAwayContext()
{
  ((IObjectContextAdapter)this).ObjectContext
    .ObjectMaterialized += (sender, args) =>
    {
      var entity = args.Entity as IObjectWithState;
      if (entity != null)
      {
        entity.State = State.Unchanged;

        entity.OriginalValues =
          BuildOriginalValues(this.Entry(entity).OriginalValues);
      }
    };
}

private static Dictionary<string, object> BuildOriginalValues(
  DbPropertyValues originalValues)
{
  var result = new Dictionary<string, object>();
  foreach (var propertyName in originalValues.PropertyNames)
  {
    var value = originalValues[propertyName];
    if (value is DbPropertyValues)
    {
      result[propertyName] =
        BuildOriginalValues((DbPropertyValues)value);
    }
    else
    {
      result[propertyName] = value;
    }
  }
  return result;
}

In addition to marking the entity as Unchanged, this updated code will populate the OriginalValues property. It does this by getting the original values from the change tracking entry for the entity and using the BuildOriginalValue helper method to convert them to the required dictionary format. The helper method loops through each of the properties that we have original values for. If the value is just a normal scalar property, it copies the value of the property to into the resulting dictionary. If the value is a DbPropertyValues, this indicates that it is a complex property and the code uses a recursive call to build a nested dictionary of the values in the complex property. More information on nested DbPropertyValues for complex properties is available in Working with Complex Properties.

Because the entity has just been returned from the database, the current and original values are the same, so we could have also used the CurrentValues property to get the values. Now we can update the ApplyChanges method to make use of this property (Example 4-24).

Example 4-24. Using original values in ApplyChanges

private static void ApplyChanges<TEntity>(TEntity root)
  where TEntity : class, IObjectWithState
{
  using (var context = new BreakAwayContext())
  {
    context.Set<TEntity>().Add(root);

    CheckForEntitiesWithoutStateInterface(context);

    foreach (var entry in context.ChangeTracker
      .Entries<IObjectWithState>())
    {
      IObjectWithState stateInfo = entry.Entity;
      entry.State = ConvertState(stateInfo.State);
      if (stateInfo.State == State.Unchanged)
      {
        ApplyPropertyChanges(entry.OriginalValues,
         stateInfo.OriginalValues);
      }
    }

    context.SaveChanges();
  }
}

private static void ApplyPropertyChanges(
  DbPropertyValues values,
  Dictionary<string, object> originalValues)
{
  foreach (var originalValue in originalValues)
  {
    if (originalValue.Value is Dictionary<string, object>)
    {
      ApplyPropertyChanges(
        (DbPropertyValues)values[originalValue.Key],
        (Dictionary<string, object>)originalValue.Value);
    }
    else
    {
      values[originalValue.Key] = originalValue.Value;
    }
  }
}

After painting the state of entities throughout the graph, the code now checks to see if it’s an existing entity. For existing entities the code uses the ApplyPropertyChanges helper method to set the original values for the entity. The helper method loops through the OriginalValues that were captured when the entity was retrieved from the database. If the value is a nested dictionary, indicating a complex property, then it uses a recursive call to apply the changes for the individual properties inside the complex property. If the value is just a scalar value, it updates the original value being stored by the context. Entity Framework will detect if any of the values differ from the values currently assigned to the properties of the entity. If a difference is detected, the property, and therefore the entity, will be marked as modified. We also need to update the ConvertState method because we no longer have a Modified option in the State enum (Example 4-25).

Example 4-25. ConvertState updated to reflect removal of Modified state

public static EntityState ConvertState(State state)
{
  switch (state)
  {
    case State.Added:
      return EntityState.Added;

    case State.Deleted:
      return EntityState.Deleted;

    default:
      return EntityState.Unchanged;
  }
}

To test out the new logic, you can update the TestSaveDestinationGraph method to no longer mark entities as Modified when it changes properties (Example 4-26). This is no longer required because the ApplyChanges method will calculate this for you.

Example 4-26. Updating the test method to test recording of original values

private static void TestSaveDestinationGraph()
{
  Destination canyon;
  using (var context = new BreakAwayContext())
  {
    canyon = (from d in context.Destinations.Include(d => d.Lodgings)
              where d.Name == "Grand Canyon"
              select d).Single();
  }

  canyon.TravelWarnings = "Carry enough water!";

  var firstLodging = canyon.Lodgings.First();
  firstLodging.Name = "New Name Holiday Park";

  var secondLodging = canyon.Lodgings.Last();
  secondLodging.State = State.Deleted;

  canyon.Lodgings.Add(new Lodging
  {
    Name = "Big Canyon Lodge",
    State = State.Added
  });

  ApplyChanges(canyon);
}

If you run the application, you will get the familiar set of SQL statements from Figure 4-3. The update statements that are generated will only set properties that were actually modified.

Concurrency implications

This approach offers the best concurrency support because it records the same information that is stored by the change tracker when modifying entities that are attached to a context. Timestamp concurrency properties will work because the value retrieved from the database is sent to the client and then back to the server, to be used when updating existing data. Concurrency properties that can be modified will also work because the original value, which was assigned to the property when it was retrieved from the database, is recorded. Because this value is set as the original value for the property, Entity Framework will use the original value when performing concurrency checks.

Querying and Applying Changes

Another approach that developers sometimes try is to calculate the modified properties by querying the database to get the current entity and then copying the values from the incoming entity. Because this approach requires one query to get the entity from the database and often a second query to update the data, it’s usually slower than just marking the entity as modified and updating every column. That said, sending a lot of unnecessary updates to the database isn’t ideal. If one of the other techniques in this chapter doesn’t work for you, this may be worth looking at.

Entity Framework makes it easy to copy the values from one object to another. Putting graphs aside for a moment, we could implement an UpdateDestination method as follows:

public static void UpdateDestination(Destination destination)
{
  using (var context = new BreakAwayContext())
  {
    if (destination.DestinationId > 0)
    {
      var existingDestiantion = context.Destinations
        .Find(destination.DestinationId);

      context.Entry(existingDestiantion)
        .CurrentValues
        .SetValues(destination);
    }
    else
    {
      context.Destinations.Add(destination);
    }

    context.SaveChanges();
  }
}

If the Destination has a key value assigned, it’s assumed to be an existing Destination. The Find method is used to load the Destination from the database. The SetValues method is used on CurrentValues to copy values from the incoming Destination to the existing Destination from the database. Entity Framework will automatically detect if any of the property values are different. If there are differences, the appropriate properties will be marked as modified.

This approach falls down when you start working with graphs, though. Let’s assume the incoming Destination references a new Lodging. We can’t query for this Lodging from the database, since it’s new, so we need to register this Lodging for addition. The problem is if we try and add the Lodging to the context, it will also try and add any other entities that it references. If the Lodging references an existing entity, it’s going to end up in the context in the added state. This gets very complicated, because we now want to try and take this existing entity back out of the context so that we can query for the entity from the database and copy its values over.

While it is technically possible to make this work, the code gets very complicated. Fortunately, there is a better alternative. Rather than getting the existing entity from the database and copying the values to it, we can attach the incoming entity and then set its original values to the values from the database. Update the ApplyChanges method as shown in Example 4-27.

Example 4-27. ApplyChanges checks for modification using database values

private static void ApplyChanges<TEntity>(TEntity root)
  where TEntity : class, IObjectWithState
{
  using (var context = new BreakAwayContext())
  {
    context.Set<TEntity>().Add(root);

    CheckForEntitiesWithoutStateInterface(context);

    foreach (var entry in context.ChangeTracker
      .Entries<IObjectWithState>())
    {
      IObjectWithState stateInfo = entry.Entity;
      entry.State = ConvertState(stateInfo.State);
      if (stateInfo.State == State.Unchanged)
      {
        var databaseValues = entry.GetDatabaseValues();
        entry.OriginalValues.SetValues(databaseValues);
      }
    }

    context.SaveChanges();
  }
}

Rather than getting the original values that were recorded when the entity was retrieved from the database, the code now gets the current original values from the database. In fact, the OriginalValues property on IObjectWithState is now no longer required. The database values are retrieved using the GetDatabaseValues method. This method returns DbPropertyValues, which is the same type returned from the CurrentValues and OriginalValues property on an entity. The SetValues method on DbPropertyValues will copy the values from any other DbPropertyValues instance into itself. We use this functionality to copy the database values into the original values for each entity. If you run the application, SQL statements similar to those from Figure 4-3 will be executed against the database. However, this time the update statements will only set the properties that were actually changed.

Concurrency implications

This approach bypasses concurrency checks because it requeries for the database values just before saving any changes. These new database values are used in place of the values that were originally retrieved from the database when sending data to the client. If you are using concurrency tokens, this approach to replaying changes on the server is not suitable.

With Safari, you learn the way you learn best. Get unlimited access to videos, live online training, learning paths, books, interactive tutorials, and more.

Start Free Trial

No credit card required