O'Reilly logo

Programming Entity Framework: Code First 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

Working with Cascade Delete

Cascade delete allows dependent data to be automatically deleted when the principal record is deleted. If you delete a Destination, for example, the related Lodgings will also be deleted automatically. Entity Framework supports cascade delete behavior for in-memory data as well as in the database. As discussed in Chapter 19 of the second edition of Programming Entity Framework, it is recommended that you implement cascade delete on entities in the model if their mapped database objects also have cascade delete defined.

By convention, Code First switches on cascade delete for required relationships. When a cascade delete is defined, Code First will also configure a cascade delete in the database that it creates. Earlier in this chapter we looked at making the Lodging to Destination relationship required. In other words, a Lodging cannot exist without a Destination. Therefore, if a Destination is deleted, any related Lodgings (that are in memory and being change-tracked by the context) will also be deleted. When SaveChanges is called, the database will delete any related rows that remain in the Lodgings table, using its cascade delete behavior.

Looking at the database, you can see that Code First carried through the cascade delete and set up a constraint on the relationship in the database. Notice the Delete Rule in Figure 4-7 is set to Cascade.

Example 4-12 shows a new method called DeleteDestinationInMemoryAndDbCascade, which we’ll use to demonstrate the in-memory and database cascade delete.

The code uses a context to insert a new Destination with a couple of Lodgings. It then saves these Lodgings to the database and records the primary of the new Destination. In a separate context, the code then retrieves the Destination and its related Lodgings, and then uses the Remove method to mark the Destination instance as Deleted. We use Console.WriteLine to inspect the state of one of the related Lodging instances that are in memory. We’ll do this using the Entry method of DbContext. The Entry method gives us access to the information that EF has about the state of a given object. Next, the call to SaveChanges persists the deletions to the database.

Cascade delete defined in a database constraint

Figure 4-7. Cascade delete defined in a database constraint

Example 4-12. A method to explore cascade deletes

private static void DeleteDestinationInMemoryAndDbCascade()
{
  int destinationId;
  using (var context = new BreakAwayContext())
  {
    var destination = new Destination
    {
      Name = "Sample Destination",
      Lodgings = new List<Lodging>
      {
        new Lodging { Name = "Lodging One" },
        new Lodging { Name = "Lodging Two" }
      }
    };

    context.Destinations.Add(destination);
    context.SaveChanges();
    destinationId = destination.DestinationId;
  }

  using (var context = new BreakAwayContext())
  {
    var destination = context.Destinations
      .Include("Lodgings")
      .Single(d => d.DestinationId == destinationId);

    var aLodging = destination.Lodgings.FirstOrDefault();
    context.Destinations.Remove(destination);

    Console.WriteLine("State of one Lodging: {0}",
      context.Entry(aLodging).State.ToString());

    context.SaveChanges();
  }
}

After calling Remove on the Destination, the state of a Lodging is displayed in the console window. It is Deleted also even though we did not explicitly remove any of the Lodgings. That’s because Entity Framework used client-side cascade deleting to delete the dependent Lodgings when the code explicitly deleted (Removed) the destination.

Next, when SaveChanges is called, Entity Framework sent three DELETE commands to the database, as shown in Figure 4-8. The first two are to delete the related Lodging instances that were in memory and the third to delete the Destination.

Delete commands in response to deleting a Destination and its related Lodgings

Figure 4-8. Delete commands in response to deleting a Destination and its related Lodgings

Now let’s change the method. We’ll remove the eager loading (Include) that pulled the Lodging data into memory along with Destination. We’ll also remove all of the related code that mentions the Lodgings. Since there are no Lodgings in memory, there will be no client-side cascade delete, but the database should clean up any orphaned Lodgings because of the cascade delete defined in the database (Figure 4-7). The revised method is listed in Example 4-13.

Example 4-13. Modified DeleteDestinationInMemoryAndDbCascade code

private static void DeleteDestinationInMemoryAndDbCascade()
{
  int destinationId;
  using (var context = new BreakAwayContext())
  {
    var destination = new Destination
    {
      Name = "Sample Destination",
      Lodgings = new List<Lodging>
      {
        new Lodging { Name = "Lodging One" },
        new Lodging { Name = "Lodging Two" }
      }
    };

    context.Destinations.Add(destination);
    context.SaveChanges();
    destinationId = destination.DestinationId;
  }

  using (var context = new BreakAwayContext())
  {
    var destination = context.Destinations
      .Single(d => d.DestinationId == destinationId);

    context.Destinations.Remove(destination);
    context.SaveChanges();
  }

  using (var context = new BreakAwayContext())
  {
    var lodgings = context.Lodgings
      .Where(l => l.DestinationId == destinationId).ToList();

    Console.WriteLine("Lodgings: {0}", lodgings.Count);
  }
}

When run, the only command sent to the database is one to delete the destination. The database cascade delete will delete the related lodgings in response. When querying for the Lodgings at the end, since the database deleted the lodgings, the query will return no results and the lodgings variable will be an empty list.

Turning On or Off Client-Side Cascade Delete with Fluent Configurations

You might be working with an existing database that does not use cascade delete or you may have a policy of being explicit about data removal and not letting it happen automatically in the database. If the relationship from Lodging to Destination is optional, this is not a problem, since by convention, Code First won’t use cascade delete with an optional relationship. But you may want a required relationship in your classes without leveraging cascade delete.

You may want to get an error if the user of your application tries to delete a Destination and hasn’t explicitly deleted or reassigned the Lodging instances assigned to it.

For the scenarios where you want a required relationship but no cascade delete, you can explicitly override the convention and configure cascade delete behavior with the Fluent API. This is not supported with Data Annotations.

Keep in mind that if you set the model up this way, your application code will be responsible for deleting or reassigning dependent data when necessary.

The Fluent API method to use is called WillCascadeOnDelete and takes a Boolean as a parameter. This configuration is applied to a relationship, which means that you first need to specify the relationship using a Has/With pairing and then call WillCascadeOnDelete.

Working within the LodgingConfiguration class, the relationship is defined as:

HasRequired(l=>l.Destination)
 .WithMany(d=>d.Lodgings)

From there, you’ll find three possible configurations to add. WillCascadeOnDelete is one of them, as you can see in Figure 4-9.

WillCascadeOnDelete—one of the configurations you can add to a fluently described relationship

Figure 4-9. WillCascadeOnDelete—one of the configurations you can add to a fluently described relationship

Now you can set WillCascadeOnDelete to false for this relationship:

HasRequired(l=>l.Destination)
  .WithMany(d=>d.Lodgings)
  .WillCascadeOnDelete(false)

Warning

If you add the above code to your project, remove it again before continuing with the rest of this chapter.

This will also mean that the database schema that Code First generates will not include the cascade delete. The Delete Rule that was Cascade in Figure 4-7 would become No Action.

In the scenario where the relationship is required, you’ll need to be aware of logic that will create a conflict, for example, the current required relationship between Lodging and Destination that requires that a Lodging instance have a Destination or a DestinationId. If you have a Lodging that is being change-tracked and you delete its related Destination, this will cause Lodging.Destination to become null. When SaveChanges is called, Entity Framework will attempt to synchronize Lodging.DestinationId, setting it to null. But that’s not possible and an exception will be thrown with the following detailed message:

The relationship could not be changed because one or more of the foreign-key properties is non-nullable. When a change is made to a relationship, the related foreign-key property is set to a null value. If the foreign-key does not support null values, a new relationship must be defined, the foreign-key property must be assigned another non-null value, or the unrelated object must be deleted.

The overall message here is that you have control over the cascade delete setting, but you will be responsible for avoiding or resolving possible validation conflicts caused by not having a cascade delete present.

Setting Cascade Delete Off in Scenarios That Are Not Supported by the Database

Some databases (including SQL Server) don’t support multiple relationships that specify cascade delete pointing to the same table. Because Code First configures required relationships to have cascade delete, this results in an error if you have two required relationships to the same entity. You can use WillCascadeOnDelete(false) to turn off the cascade delete setting on one or more of the relationships. Example 4-14 shows an example of the exception message from SQL Server if you don’t configure this correctly.

Example 4-14. Exception message when Code First attempts to create cascade delete where multiple relationships exist

System.InvalidOperationException was unhandled
  Message=The database creation succeeded, but the creation of the database objects
           did not.
  See InnerException for details.

 InnerException: System.Data.SqlClient.SqlException
   Message=Introducing FOREIGN KEY constraint 'Lodging_SecondaryContact' on table
           'Lodgings' may cause cycles or multiple cascade paths. Specify ON DELETE
            NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY
            constraints. Could not create constraint. See previous errors.

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