Implementing EF4 Change Interceptors

Interceptors are a great way to handle some repetitive and predictable data management tasks. NHibernate has good support for Interceptors both at the change and query levels. I wondered how hard it would be to write interceptors for the new EF4 CTP and was surprised at how easy it actually was… well, for the change interceptors anyway. It looks like query interceptors would require a complete reimplementation of the Linq provider—not something I feel like undertaking right now.

On to the Code!

This is the first interface we’ll use to create a class that can respond to changes in the EF4 data context.

  1. namespace Yodelay.Data.Entity
  2. {
  3.     /// <summary>
  4.     /// Interface to support taking some action in response
  5.     /// to some activity taking place on an ObjectStateEntry item.
  6.     /// </summary>
  7.     public interface IInterceptor
  8.     {
  9.         void Before(ObjectStateEntry item);
  10.         void After(ObjectStateEntry item);
  11.     }
  12. }

 

We’ll also use this interface to add some conditional execution support.

  1. namespace Yodelay.Data.Entity
  2. {
  3.     /// <summary>
  4.     /// Adds conditional execution to an IInterceptor.
  5.     /// </summary>
  6.     public interface IConditionalInterceptor : IInterceptor
  7.     {
  8.         bool IsTargetEntity(ObjectStateEntry item);        
  9.     }
  10. }

 

The first interceptor I want to write is one that manages four audit columns automatically. First I need an interface that provides the audit columns:

  1. public interface IAuditEntity
  2. {
  3.     DateTime InsertDateTime { get; set; }
  4.     DateTime UpdateDateTime { get; set; }
  5.     string InsertUser { get; set; }
  6.     string UpdateUser { get; set; }
  7. }

The EF4 DbContext class provides an override for SaveChanges() that I can use to start handling the events. I decided to subclass DbContext and add the interception capability to the new class. I snipped the constructors for brevity, but all of the constructors from the base class are bubbled.

  1. public class DataContext : DbContext
  2. {
  3.     private readonly List<IInterceptor> _interceptors = new List<IInterceptor>();
  4.     public List<IInterceptor> Interceptors
  5.     {
  6.         get { return this._interceptors; }
  7.     }
  8.  
  9.     private void InterceptBefore(ObjectStateEntry item)
  10.     {
  11.         this.Interceptors.ForEach(intercept => intercept.Before(item));
  12.     }
  13.  
  14.     private void InterceptAfter(ObjectStateEntry item)
  15.     {
  16.         this.Interceptors.ForEach(intercept => intercept.After(item));
  17.     }
  18.  
  19.     public override int SaveChanges()
  20.     {
  21.         const EntityState entitiesToTrack = EntityState.Added |
  22.                                             EntityState.Modified |
  23.                                             EntityState.Deleted;
  24.         var elementsToSave =
  25.             this.ObjectContext
  26.                 .ObjectStateManager
  27.                 .GetObjectStateEntries(entitiesToTrack)
  28.                 .ToList();
  29.  
  30.         elementsToSave.ForEach(InterceptBefore);
  31.  
  32.         var result = base.SaveChanges();
  33.         elementsToSave.ForEach(InterceptAfter);
  34.         return result;
  35.     }

 

I only want the AuditChangeInterceptor to fire if the object implements the IAuditEntity interface. I could have directly implemented IConditionalInterceptor, but I decided to extract the object-type criteria into a super-class.

  1. public abstract class TypeInterceptor : IConditionalInterceptor
  2. {
  3.     private readonly System.Type _targetType;
  4.     public Type TargetType { get { return _targetType; }}
  5.  
  6.     protected TypeInterceptor(System.Type targetType)
  7.     {
  8.         this._targetType = targetType;
  9.     }
  10.  
  11.     public virtual bool IsTargetEntity(ObjectStateEntry item)
  12.     {
  13.         return item.State != EntityState.Detached &&
  14.             this.TargetType.IsInstanceOfType(item.Entity);
  15.     }
  16.  
  17.     public void Before(ObjectStateEntry item)
  18.     {
  19.         if (this.IsTargetEntity(item))
  20.             this.OnBefore(item);
  21.     }
  22.  
  23.     protected abstract void OnBefore(ObjectStateEntry item);
  24.  
  25.     public void After(ObjectStateEntry item)
  26.     {
  27.         if (this.IsTargetEntity(item))
  28.             this.OnAfter(item);
  29.     }
  30.  
  31.     protected abstract void OnAfter(ObjectStateEntry item);
  32. }

 

I also decided that the super-class should provide obvious method-overrides for BeforeInsert, AfterInsert, BeforeUpdate, etc.. For that I created a generic class that sub-classes TypeInterceptor and provides friendlier methods to work with.

  1. public class ChangeInterceptor<T> : TypeInterceptor
  2. {
  3.     #region Overrides of Interceptor
  4.  
  5.     protected override void OnBefore(ObjectStateEntry item)
  6.     {
  7.         T tItem = (T) item.Entity;
  8.         switch(item.State)
  9.         {
  10.             case EntityState.Added:
  11.                 this.OnBeforeInsert(item.ObjectStateManager, tItem);
  12.                 break;
  13.             case EntityState.Deleted:
  14.                 this.OnBeforeDelete(item.ObjectStateManager, tItem);
  15.                 break;
  16.             case EntityState.Modified:
  17.                 this.OnBeforeUpdate(item.ObjectStateManager, tItem);
  18.                 break;
  19.         }
  20.     }
  21.  
  22.     protected override void  OnAfter(ObjectStateEntry item)
  23.     {
  24.         T tItem = (T)item.Entity;
  25.         switch (item.State)
  26.         {
  27.             case EntityState.Added:
  28.                 this.OnAfterInsert(item.ObjectStateManager, tItem);
  29.                 break;
  30.             case EntityState.Deleted:
  31.                 this.OnAfterDelete(item.ObjectStateManager, tItem);
  32.                 break;
  33.             case EntityState.Modified:
  34.                 this.OnAfterUpdate(item.ObjectStateManager, tItem);
  35.                 break;
  36.         }        
  37.     }
  38.  
  39.     #endregion
  40.  
  41.     public virtual void OnBeforeInsert(ObjectStateManager manager, T item)
  42.     {
  43.         return;
  44.     }
  45.  
  46.     public virtual void OnAfterInsert(ObjectStateManager manager, T item)
  47.     {
  48.         return;
  49.     }
  50.  
  51.     public virtual void OnBeforeUpdate(ObjectStateManager manager, T item)
  52.     {
  53.         return;
  54.     }
  55.  
  56.     public virtual void OnAfterUpdate(ObjectStateManager manager, T item)
  57.     {
  58.         return;
  59.     }
  60.  
  61.     public virtual void OnBeforeDelete(ObjectStateManager manager, T item)
  62.     {
  63.         return;
  64.     }
  65.  
  66.     public virtual void OnAfterDelete(ObjectStateManager manager, T item)
  67.     {
  68.         return;
  69.     }
  70.  
  71.     public ChangeInterceptor() : base(typeof(T))
  72.     {
  73.         
  74.     }
  75. }

 

Finally, I created subclassed ChangeInterceptor<IAuditEntity>.

  1. public class AuditChangeInterceptor : ChangeInterceptor<IAuditEntity>
  2. {
  3.     public override void OnBeforeInsert(ObjectStateManager manager, IAuditEntity item)
  4.     {
  5.         base.OnBeforeInsert(manager, item);
  6.  
  7.         item.InsertDateTime = DateTime.Now;
  8.         item.InsertUser = System.Threading.Thread.CurrentPrincipal.Identity.Name;
  9.         item.UpdateDateTime = DateTime.Now;
  10.         item.UpdateUser = System.Threading.Thread.CurrentPrincipal.Identity.Name;
  11.     }
  12.  
  13.     public override void OnBeforeUpdate(ObjectStateManager manager, IAuditEntity item)
  14.     {
  15.         base.OnBeforeUpdate(manager, item);
  16.         item.UpdateDateTime = DateTime.Now;
  17.         item.UpdateUser = System.Threading.Thread.CurrentPrincipal.Identity.Name;
  18.     }
  19. }

 

I plugged this into my app, and it worked on the first go.

Another common scenario I encounter is “soft-deletes.” A “soft-delete” is a virtual record deletion that does not actual remove the record from the database. Instead it sets an IsDeleted flag on the record, and the record is then excluded from other queries. The problem with soft-deletes is that developers and report writers always have to remember to add the “IsDeleted == false” criteria to every query in the system that touches the affected records. It would be great to replace the standard delete functionality with a soft-delete, and to modify the IQueryable to return only records for which “IsDeleted == false.” Unfortunately, I was unable to find a clean way to add query-interceptors to the data model to keep deleted records from being returned. However, I was able to get the basic soft-delete ChangeInterceptor to work. Here is that code.

 

  1. public interface ISoftDelete
  2. {
  3.     bool IsDeleted { get; set; }
  4. }

 

  1. public class SoftDeleteChangeInterceptor : ChangeInterceptor<ISoftDelete>
  2. {
  3.     public override void OnBeforeInsert(ObjectStateManager manager, ISoftDelete item)
  4.     {
  5.         base.OnBeforeInsert(manager, item);
  6.         item.IsDeleted = false;
  7.     }
  8.  
  9.     public override void OnBeforeDelete(ObjectStateManager manager, ISoftDelete item)
  10.     {
  11.         if (item.IsDeleted)
  12.             throw new InvalidOperationException("Item is already deleted.");
  13.         
  14.         base.OnBeforeDelete(manager, item);
  15.         item.IsDeleted = true;
  16.         manager.ChangeObjectState(item, EntityState.Modified);
  17.     }
  18. }

 

Here’s the complete diagram of the code:

image

EF4 has come a long way with respect to supporting extensibility. It still needs query-interceptors to be feature-parable with other ORM tools such as NHibernate, but I suspect that it is just a matter of time before the MS developers get around to adding that functionality. For now, you can use the interceptor model I’ve demo’ed here to add functionality to your data models. Perhaps you could use them to add logging, validation, or security checks to your models. What can you come up with?

5 thoughts on “Implementing EF4 Change Interceptors

  1. This is a great article, I’ve been banging my head against audit logging in EF for quite a while. Thanks.

  2. Hi, im kinda new to EF and a bit puzzled when say ‘plug into your app’, are you editing the generated code for the DataContext?

    thanks

    1. This work was done against EF4 CTP4 using the code-first approach. I haven’t adapted change interceptors to either of the approaches that use auto-generated code. Assuming the generated code yields a class called “DataContext,” you could use a similar approach and sub-class “DataContext” so that you can override the SaveChanges method. Once you’ve overridden SaveChanges, you should be able to inject whatever functionality you wish. In your application code you would use your own sub-class instead of the generated one.

Leave a Reply

%d bloggers like this: