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

Building a Generic Approach to Track State Locally

The SaveDestination method we implemented in Example 4-12 isn’t overly complex, but if we expose methods to save various parts of our model, we would be repeating the state setting code over and over again in each method. So let’s take a look at a more generalized approach to applying changes on the server.

Note

You may recognize this pattern from Programming Entity Framework, 2e. It was introduced in Chapter 18 when demonstrating the user of POCOs in WCF Services.

This approach relies on having a consistent way to determine the state of any entity in your model. The easiest way to achieve that is to have an interface or abstract base class that every entity in your model will implement.

For this example we are going to use an IObjectWithState interface that will tell us the current state of the entity. It will have no dependencies on Entity Framework and will be implemented by your domain classes, so it can go in the Model project. Go ahead and add the IObjectWithState interface to the Model project (Example 4-14). Later in this section you’ll add this interface to some of your classes.

Example 4-14. Sample interface to determine state of an entity

namespace Model
{
  public interface IObjectWithState
  {
    State State { get; set; }
  }

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

Note that we’ve opted for a new enum to represent state rather than reusing the EntityState enum from Entity Framework. This ensures our domain model doesn’t have any dependencies on types from our data access technology.

Before we get to applying state, it would be useful if any entities we retrieve from the database would have their state automatically set to Unchanged. Otherwise the server needs to manually do this before returning the objects to the client. The easiest way to do this is to listen to the ObjectMaterialized event on the underlying ObjectContext. Add the constructor in Example 4-15 to the BreakAwayContext class. You’ll need to add a using statement for the System.Data.Entity.Infrastructure namespace to get access to the IObjectContextAdapter interface.

Example 4-15. Hooking up an event to mark existing entities as Unchanged

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

The code uses IObjectContextAdapter to get access to the underlying ObjectContext. It then wires up a new handler to the ObjectMaterialized event, which will fire whenever an entity is returned from a query to the database. Because all objects that come from the database are existing objects, we take this opportunity to mark them as Unchanged if they implement our state tracking interface.

In a real-world scenario you would need to implement the change tracking interface on every class in your model. But for the sake of simplicity, we will just use Destination and Lodging for this demonstration. Go ahead and edit the Lodging and Destination classes to implement the IObjectWithState interface:

public class Destination : IObjectWithState

public class Lodging : IObjectWithState

You’ll also need to add a State property into both of these classes to satisfy the IObjectWithState interface that you just added:

public State State { get; set; }

Now it’s time to write a method that uses all this information to take a disconnected graph and apply the client-side changes to a context by setting the correct state for each entity in the graph.

Warning

One important thing to remember is that this approach is dependent on the client application honoring the contract of setting the correct state. If the client doesn’t set the correct state, the save process will not behave correctly.

Add the SaveDestinationGraph and ConvertState methods shown in Example 4-16.

Example 4-16. Setting state based on a state tracking interface

public static void SaveDestinationGraph(Destination destination)
{
  using (var context = new BreakAwayContext())
  {
    context.Destinations.Add(destination);

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

    context.SaveChanges();
  }
}

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

    case State.Modified:
      return EntityState.Modified;

    case State.Deleted:
      return EntityState.Deleted;

    default:
      return EntityState.Unchanged;
  }
}

The code uses DbSet.Add on the root Destination to get the contents of the graph into the context in the Added state. Next it uses the ChangeTracker.Entries<TEntity> method to find the entries for all entities that are tracked by the context and implement IObjectWithState.

Note

The Entries method will give you access to the same object that you would get by calling DbContext.Entry on each entity. There is a nongeneric overload of Entries that will give you an entry for every entity that is tracked by the context. The generic overload, which we are using, will filter the entries to those that are of the specified type, derived from the specified type, or implement the specified interface. If you use the generic overload, the Entity property of each entry object will be strongly typed as the type you specified. You’ll learn more about the Entries method in Chapter 5.

For each entry, the code converts the state from the entities State property to Entity Framework’s EntityState and sets it to the State property for the change tracker entry. Once all the states have been set, it’s time to use SaveChanges to push the changes to the database. Now that we have our generalized solution, let’s write some code to test it out. We’re going to apply the same changes we did back in Example 4-13, but this time using our new method of applying changes. Add the TestSaveDestinationGraph method shown in Example 4-17.

Example 4-17. Testing the new SaveDestinationTest method

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!";
  canyon.State = State.Modified;

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

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

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

  SaveDestinationGraph(canyon);
}

The code simulates a client application that queries for an existing Destination and its related Lodgings. The Destination is updated and marked as Modified. The first Lodging is also updated and marked as Modified. The second Lodging is marked for deletion by setting its State property to Deleted. Finally, a new Lodging is put into the Lodgings collection with its State set to Added. The graph is then passed to the SaveDestinationGraph method. If you update the Main method to call TestSaveDestinationGraph and run your application, the same SQL statements from Figure 4-3 will be run against the database.

Creating a Generic Method That Can Apply State Through Any Graph

With some simple tweaks to the SaveDestinationGraph method we wrote in Example 4-16, we can create a method that can work on any root in our model, not just Destinations. Add the ApplyChanges method shown in Example 4-18.

Warning

The generic method demonstrated in this section is specifically designed for use with disconnected scenarios where you have a short-lived context. Notice that the context is instantiated in the ApplyChanges method.

Example 4-18. Generalized method for replaying changes from a disconnected graph of entities

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

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

    context.SaveChanges();
  }
}

This new method accepts any root that implements IObjectWithState. Because we don’t know the type of the root until runtime, we don’t know which DbSet to add it to. Fortunately there is a Set<T> method on DbContext that can be used to create a set of a type that will be resolved at runtime. We use that to get a set and then add the root. Next we set the state for each entity in the graph and then push the changes to the database. If you want to test this new method out, change the last line of your TestSaveDestinationGraph method to call ApplyChanges rather than SaveDestinationGraph:

ApplyChanges(canyon);

Running the application will result in the same SQL statements from Figure 4-3 being run again.

There is one potential issue with our ApplyChanges method—at the moment it blindly assumes that every entity in the graph implements IObjectWithState. If an entity that doesn’t implement the interface is present, it will just be left in the Added state and Entity Framework will attempt to insert it. Update the ApplyChanges method as shown in Example 4-19.

Example 4-19. Checking for entities that don’t implement IObjectWithState

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);
    }

    context.SaveChanges();
  }
}

private static void CheckForEntitiesWithoutStateInterface(
    BreakAwayContext context)
{
  var entitiesWithoutState =
    from e in context.ChangeTracker.Entries()
    where !(e.Entity is IObjectWithState)
    select e;

  if (entitiesWithoutState.Any())
  {
    throw new NotSupportedException(
      "All entities must implement IObjectWithState");
  }
}

The method now calls a CheckForEntitiesWithoutStateInterface helper method that uses the nongeneric overload of ChangeTracker.Entries to get all entities that have been added to the context. It uses a LINQ query to find any of these that don’t implement IObjectWithState. If any entities that don’t implement IObjectWithState are found, an exception is thrown.

Concurrency Implications

This approach works well with timestamp-style concurrency tokens, where the property that is used as a concurrency token will not be updated on the client. For existing entities, the value of the timestamp property will be sent to the client and then back to the server. The entity will then be registered as Unchanged, Modified, or Deleted with the same value in the concurrency token property as when it was originally queried from the database.

If you need to have concurrency checking in your N-Tier application, timestamp properties are arguably the easiest way to implement this. More information on timestamp properties in Code First models is available in Chapter 3 of Programming Entity Framework: Code First. For Database First and Model First, see Chapter 23 of Programming Entity Framework, 2e.

If a property that can be updated on the client is used as a concurrency token, this approach will not suffice. Because this approach does not track the original value of properties, the concurrency token will only have the updated value for the concurrency check. This value will be checked against the database value during save, and a concurrency exception will be thrown because it will not match the value in the database. To overcome this limitation you will need to use the approach described in Recording Original Values.

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