Soft Deleting Entities Cleanly Using Entity Framework 6 Interceptors

Overview

A ‘DELETE’ command sent to a database will permanently remove the rows applicable. This might not be acceptable if the command is sent accidentally or because of a bug in the application code. The database restore from the backup may not always be feasible, especially in production environments. So, it is a very common requirement to soft delete entries by using a flag in the table. This flag can be turned on to tell the application logic to consider a particular row as if it is deleted. If we realize that it is an accidental delete, we can undo it by setting the flag to false. Entity Framework (EF) does not have this feature built in. However, we can use the building blocks available in EF 6 to achieve this. Entity Framework 6 has introduced two new features: ‘Interceptors’ and ‘Custom Code Conventions’. In this article, we are going to explore more about these features and understand how these can be used to achieve soft deletes in a cleaner way.

What Are Entity Framework Interceptors?

An interceptor lets you grab the commands before they are sent to the database. The most common use of interceptors is for logging of database interactions. We can use the same concept to solve more complex problems like soft deletes, applying permanent filters for database queries, and so forth.

An interceptor is built around the concept of interception interface. Any interception interface has to inherit from IDbInterceptor and define some methods that will be called while EF executes some commands.

Implementing an Interceptor

To add a new interceptor to the EF execution pipeline, we need to execute the following steps.

1. Implement an Interface

You need to implement an interface that is inherited from IDbInterceptor.

There are few interfaces available in EF 6 that can be implemented to construct an interceptor. IDbConfigurationInterceptor, IDbCommandInterceptor, and IDbConnectionInterceptor are some of them. All of these inherit the IDbInterceptor. The intent is to have one interface per type of object being intercepted.

2. Register the Interceptor

Once we implement an interceptor, we can register it by:

  1. Using the DbInterception class as DbInterception.Add(new CustomInterceptor ());
  2. Using DbConfigurationas follows:
    public class CustomConfiguration : DbConfiguration
    {
       public CustomConfiguration()
       {
          AddInterceptor(new CustomInterceptor ());
       }
    }
    
  3. Using the configuration file as an interceptor element under the interceptorschild section.
    <interceptors>
       <interceptor type=
          "ContosoUniversity.DAL.SoftDeleteInterceptor,
          ContosoUniversity"
    </interceptors>
    

3. Interception Context

All the methods that are part of the interceptor interface receive an object of type DbInterceptionContext or some other type derived from this. This object contains the contextual information about the action that the EF is taking. For example, the IDbCommandTreeInterceptor interface has a method, ‘TreeCreated’, that accepts an object of typeDbCommandTreeInterceptionContext. This object allows us to tweak the command tree before being executed on the database.

Custom Code Conventions

In the previous section, we discussed the concept of ‘Interceptors’ and learned the steps to follow to successfully add a new interceptor. In this section, we are going to discuss another new feature in EF 6, called ‘Custom Code Conventions’.

A convention allows you to set up a policy that a model will follow. The built-in conventions are designed to target the most common scenarios. However, it is a common requirement in code first to configure the mappings that don’t align with the built-in convention. The Custom code first conventions let you to override the built-in conventions.

There are a number of ways to configure custom conventions:

  1. By Type
    protected override void OnModelCreating
       (DbModelBuilder modelBuilder)
    {
       modelBuilder.Properties<string>().
          Configure(s => s.HasMaxLength(50));
    }
    

    In the preceding example, we are configuring the model to set all ‘string' columns in your database to be varchar(50).

  2. By Predicate
    protected override void OnModelCreating
       (DbModelBuilder modelBuilder)
    {
       modelBuilder.Properties().Where(prop =>
       prop.Name.EndsWith("Key")).Configure(c =>
          c.IsKey());
    }
    

    The entity framework’s built-in convention for keys automatically finds properties that are named ‘Id’ or ‘Class Name + Id’ whether they are ‘int’, ‘Guid’, or ‘string’. The previous example is overriding that built-in convention and configuring the model to set all properties whose name ends with ‘Key’ as the keys in the database.

  3. Combination of Both
    protected override void OnModelCreating
       (DbModelBuilder modelBuilder)
    {
       modelBuilder.Properties<Guid>().Where
          (p => p.Name == "Key").Configure(c =>
             c.IsKey());
    }
    

    In this example, we are configuring the model to treat all the properties of type ‘Guid’ and have names ending with ‘Key’ as keys.

  4. Configure Types
    protected override void OnModelCreating
       (DbModelBuilder modelBuilder)
    {
       modelBuilder.Type<IHistory>().Configure(p =>
          p.ToTable(p.ClrType.Name, "History"));
    }
    

    In the example above, we are adding a convention where any type implementing an interface ‘IHistory‘ will be added to the ‘History’ schema in the database.

  5. Encapsulation of convention

    There is a convention class that you can use with your model builder to encapsulate your conventions. The most common reason to encapsulate your conventions is to re-use them by placing them in a separate assembly and share them across projects.

    class CustomKeyConvention : Convention
    {
       public CustomKeyConvention()
       {
          Properties()
          .Where(prop => prop.Name.EndsWith("Key"))
          .Configure(config => config.IsKey());
       }
    }
    protected override void OnModelCreating
       (DbModelBuilder modelBuilder)
    {
       modelBuilder.Conventions.Add<CustomKeyConvention>();
    }
    
  6. Using Custom Annotations

    The Custom conventions feature also allows you to define custom annotations and then wire them up to a custom convention that you defined in the fluent mappings. You need to implement the following steps to add a new custom convention through a custom annotation.

    1. Define an attribute.

    [AttributeUsage(AttributeTargets.Property,
       AllowMultiple=false, Inherited=true)]
    public class UniCodeAttribute : Attribute
    {
       public UniCodeAttribute()
          : this(true){}
       public UniCodeAttribute(bool isUniCode)
       {
          IsUnicode = isUniCode;
       }
       public bool IsUnicode { get; private set; }
    
    }
    

    2. Create a convention to respond to the attribute.

    public class CharSetConvention : Convention
    {
       public CharSetConvention()
       {
          Properties<string>().Having(p =>
          p.GetCustomAttributes(false).OfType
             <UniCodeAttribute>()).Configure((c, a)  =>
          {
             if (a.Any())
                c.IsUnicode(a.First().IsUnicode);
          });
       }
    }
    

    3. Add the convention to the model builder.

    modelBuilder.Conventions.Add<CharSetConvention>();

Soft Delete

In the preceding sections, we discussed ‘Interceptors’ and ‘Custom Code Conventions’. In this section, we are going to understand how these two concepts can be used to achieve soft deletes in an application.

We will add a custom attribute to indicate that we want to use a particular column to flag soft deletes.

public class SoftDeleteAttribute : Attribute
{
   public SoftDeleteAttribute(string column)
   {
      ColumnName = column;
   }

   public string ColumnName { get; set; }

   public static string
      GetSoftDeleteColumnName(EdmType type)
   {
      var annotation = type.MetadataProperties
      .Where(p => p.Name.EndsWith
         ("customannotation:SoftDeleteColumnName"))
      .SingleOrDefault();

      return annotation == null ? null :
         (string)annotation.Value;
   }
}

We will add a custom convention that will read the attribute from our class and turn it into a model annotation. This annotation is some extra metadata that we stuff in the model. We use that annotation while interacting with the database.

var conv = new AttributeToTableAnnotationConvention
      <SoftDeleteAttribute, string>(
   "SoftDeleteColumnName",
   (type, attributes) => attributes.Single().ColumnName);
   modelBuilder.Conventions.Add(conv);

We will add a command tree interceptor that intercepts the query when it is still an object model in the query pipe line. The interceptor adds some additional filters on that before it being sent to the provider for converting to SQL. The interceptor will also replace a DELETE command with an update command.

public class SoftDeleteInterceptor :
   IDbCommandTreeInterceptor
{
   public void TreeCreated(DbCommandTreeInterceptionContext
      interceptionContext)
   {
      if (interceptionContext.OriginalResult.DataSpace ==
         DataSpace.SSpace)
      {
         var queryCommand = interceptionContext.Result as
            DbQueryCommandTree;
         if (queryCommand != null)
         {
            var newQuery = queryCommand.Query.Accept(new
               SoftDeleteQueryVisitor());
            interceptionContext.Result = new DbQueryCommandTree(
               queryCommand.MetadataWorkspace,
               queryCommand.DataSpace,
               newQuery);
         }

         var deleteCommand = interceptionContext.OriginalResult
            as DbDeleteCommandTree;
         if (deleteCommand != null)
         {
            var column = SoftDeleteAttribute.
               GetSoftDeleteColumnName
               (deleteCommand.Target.VariableType.EdmType);
            if (column != null)
            {
               var setClauses = new List<DbModificationClause>();
               var table = (EntityType)deleteCommand.Target.
                  VariableType.EdmType;
               if (table.Properties.Any(p => p.Name == column))
               {
                  setClauses.Add(DbExpressionBuilder.SetClause(
                     DbExpressionBuilder.Property(
                        DbExpressionBuilder.Variable(deleteCommand.
                           Target.VariableType,
                           deleteCommand.Target.VariableName),
                           column),
                        DbExpression.FromBoolean(true)));
               }

               var update = new DbUpdateCommandTree(
                  deleteCommand.MetadataWorkspace,
                  deleteCommand.DataSpace,
                  deleteCommand.Target,
                  deleteCommand.Predicate,
                  setClauses.AsReadOnly(), null);

               interceptionContext.Result = update;
            }
         }
      }
   }
}

The interceptor has two parts in it. In the first part, the ‘SoftDeleteQueryVisitor’ will add a filter that removes the deleted entries when queried from the database. The second part of the interceptor is modifying a delete command into an update command.

Source Code

I’ve applied the soft delete to the course entity in the Contoso University example. You can download it from here. This application is in ASP.NET MVC and demonstrates the technique discussed above.

To run this application, you would need Visual Studio 2013 with MVC 5 installed. Once you open the solution, you can go to Manage Nuget Package on the ‘ContosoUniversity’ project and click the ‘Restore’ button to restore the missing packages.

Conclusion

In this article, we have explored some of the features in Entity Framework 6 and learned how to use them to implement soft deletes in a cleaner way. This technique enables the application to treat a soft delete enabled entity also as any other entity. This is just an example of how best we can use the extensibility points provided by EF to implement some of the most common requirements.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read