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

Setting the State for Multiple Entities in an Entity Graph

Now that you know the fundamental building blocks, it’s time to plug them together to determine and set the state of each entity in a graph. When a disconnected entity graph arrives on the server side, the server will not know the state of the entities. You need to provide a way for the state to be discovered so that the context can be made aware of each entity’s state. This section will demonstrate how you can coerce the context to infer and then apply entity state.

The first step is to get the graph into the context. You do that by performing an operation that will cause the context to start tracking the root of the graph. Once that is done, you can set the state for each entity in the graph.

Getting the Graph into the Context

Back in Example 4-1 you saw that adding the root of a graph will cause every entity in the graph to be registered with the context as a new entity. This behavior is the same if you use DbSet.Add or change the State property for an entity to Added. Once all the entities are tracked by the state manager, you can then work your way around the graph, specifying the correct state for each entity. It is possible to start by calling an operation that will register the root as an existing entity. This includes DbSet.Attach or setting the State property to Unchanged, Modified, or Deleted. However, this approach isn’t recommended because you run the risk of exceptions due to duplicate key values if you have added entities in your graph. If you register the root as an existing entity, every entity in the graph will get registered as an existing entity. Because existing entities should all have unique primary keys, Entity Framework will ensure that you don’t register two existing entities of the same type with the same key. If you have a new entity instance that will have its primary key value generated by the database, you probably won’t bother assigning a value to the primary key; you’ll just leave it set to the default value. This means if your graph contains multiple new entities of the same type, they will have the same key value. If you attach the root, Entity Framework will attempt to mark every entity as Unchanged, which will fail because you would have two existing entities with the same key.

For example, assume you have an existing Destination that includes two new instances of the Lodging class in its Lodgings property. The key of Lodging is the integer property LodgingId, which is generated by the database when you save. This means the two new Lodgings both have zero assigned to their LodgingId property. If you attempt to register the root as an existing entity, the two Lodging instances will also be registered as existing entities. This will fail because that would mean there are two existing Lodgings with a key of zero.

There may be some cases where you have multiple graphs and/or individual entities that need to be registered with the context. This occurs when not all the entities you need to reason about are reachable from one root. For example, we are going to be writing a method that will save a Destination and the Lodgings that it references. Each of these entities will either be a new entity to be added or an existing entity to be updated. The method will also accept a separate list of Lodgings that should be deleted. Because these Lodgings are to be deleted, the client will probably have removed them from the Lodgings collection on the root Destination. Therefore registering the root Destination won’t be enough to register the deleted Lodgings; we’ll need to register them separately.

Table 4-1 summarizes the options along with the pros and cons that you’ve read in this section.

Table 4-1. Patterns and warnings for joining graphs to a context

Method

Result

Warnings

Add Root

Every entity in graph will be change tracked and marked with Added state

SaveChanges will attempt to insert data that may already exist in database

Attach Root

Every entity in graph will be change tracked and marked with Unchanged state

New entities will not get inserted into database and have a conflict with matching keys

Add or Attach Root, then paint state throughout graph

Entities will have correct state when painting is complete

It is recommended that you Add the root rather than attaching it to avoid key conflicts for new entities. More information is provided at the start of this section.

Setting the State of Entities in a Graph

We’re going to start by looking at an example where we iterate through the graph using our knowledge of the model and set the state for each entity throughout the graph, or painting the state. In the next section you’ll see how you can generalize this solution so that you don’t have to manually navigate the graph set the state of each entity. We’re going to write a method that will save a Destination and its related Lodgings. Deleted entities are tricky in disconnected scenarios. If you delete the entity on the client side, there’s nothing to send to the server so that it knows to delete that data in the database as well. This example demonstrates one pattern for overcoming the problem. Add the SaveDestinationAndLodgings method shown in Example 4-12.

Example 4-12. Setting state for each entity in a graph

private static void SaveDestinationAndLodgings(
  Destination destination,
  List<Lodging> deletedLodgings)
{
  // TODO: Ensure only Destinations & Lodgings are passed in

  using (var context = new BreakAwayContext())
  {
    context.Destinations.Add(destination);

    if (destination.DestinationId > 0)
    {
      context.Entry(destination).State = EntityState.Modified;
    }

    foreach (var lodging in destination.Lodgings)
    {
      if (lodging.LodgingId > 0)
      {
        context.Entry(lodging).State = EntityState.Modified;
      }
    }

    foreach (var lodging in deletedLodgings)
    {
      context.Entry(lodging).State = EntityState.Deleted;
    }

    context.SaveChanges();
  }
}

The new method accepts the Destination to be saved. This Destination may also have Lodgings related to it. The method also accepts a list of Lodgings to be deleted. These Lodgings may or may not be in the Lodgings collection of the Destination that is being saved. You’ll also notice a TODO to ensure that the client calling the method only supplied Destinations and Lodgings, because that is all that our method is expecting. If the caller were to reference an unexpected InternetSpecial from one of the Lodgings, we wouldn’t process this with our state setting logic. Validating input is good practice and isn’t related to the topic at hand, so we’ve left it out for clarity.

The code then adds the root Destination to the context, which will cause any related Lodgings to also be added. Next we are using a check on the key property to determine if this is a new or existing Destination. If the key is set to zero, it’s assumed it’s a new Destination and it’s left in the added state; if it has a value, it’s marked as a modified entity to be updated in the database. The same process is then repeated for each of the Lodgings that is referenced from the Destination.

Finally the Lodgings that are to be deleted are registered in the Deleted state. If these Lodgings are still referenced from the Destination, they are already in the state manager in the added state. If they were not referenced by the Destination, the context isn’t yet aware of them. Either way, changing the state to Deleted will register them for deletion. With the state appropriately set for every entity in the graph, it’s time to call SaveChanges.

To see the SaveDestinationAndLodgings method in action, add the TestSaveDestinationAndLodgings method shown in Example 4-13.

Example 4-13. Method to test SaveDestinationAndLodging method

private static void TestSaveDestinationAndLodgings()
{
  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.Lodgings.Add(new Lodging
  {
    Name = "Big Canyon Lodge"
  });

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

  var secondLodging = canyon.Lodgings.ElementAt(1);
  var deletedLodgings = new List<Lodging>();
  canyon.Lodgings.Remove(secondLodging);
  deletedLodgings.Add(secondLodging);

  SaveDestinationAndLodgings(canyon, deletedLodgings);
}

This method retrieves the Grand Canyon Destination from the database, using eager loading to ensure that the related Lodgings are also in memory. Next it changes the TravelWarnings property of the canyon. Then one of the Lodgings is modified and another is removed from the Lodgings property of the canyon and added to a list of Lodgings to be deleted. A new Lodging is also added to the canyon. Finally the canyon and the list of Lodgings to be deleted are passed to the SaveDestinationAndLodgings method. If you update the Main method to call TestSaveDestinationAndLodgings and run the application, a series of SQL statements will be sent to the database (Figure 4-3).

SQL statements during save after setting state for each entity

Figure 4-3. SQL statements during save after setting state for each entity

The first update is for the existing Grand Canyon Destination that we updated the TravelWarnings property on. Next is the update for the Lodging we changed the name of. Then comes the delete for the Lodging we added to the list of Lodgings to be deleted. Finally, we see the insert for the new Lodging we created and added to the Lodgings collection of the Grand Canyon Destination.

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