Pluggable Rules for Entity Framework Code First

Posted by Ricardo Peres on ASP.net Weblogs See other posts from ASP.net Weblogs or by Ricardo Peres
Published on Tue, 25 Jun 2013 13:05:04 GMT Indexed on 2013/06/25 16:22 UTC
Read the original article Hit count: 362

Suppose you want a system that lets you plug custom validation rules on your Entity Framework context. The rules would control whether an entity can be saved, updated or deleted, and would be implemented in plain .NET. Yes, I know I already talked about plugable validation in Entity Framework Code First, but this is a different approach.

An example API is in order, first, a ruleset, which will hold the collection of rules:

   1: public interface IRuleset : IDisposable
   2: {
   3:     void AddRule<T>(IRule<T> rule);
   4:     IEnumerable<IRule<T>> GetRules<T>();
   5: }

Next, a rule:

   1: public interface IRule<T>
   2: {
   3:     Boolean CanSave(T entity, DbContext ctx);
   4:     Boolean CanUpdate(T entity, DbContext ctx);
   5:     Boolean CanDelete(T entity, DbContext ctx);
   6:     String Name
   7:     {
   8:         get;
   9:     }
  10: }

Let’s analyze what we have, starting with the ruleset:

  • Only has methods for adding a rule, specific to an entity type, and to list all rules of this entity type;
  • By implementing IDisposable, we allow it to be cancelled, by disposing of it when we no longer want its rules to be applied.

A rule, on the other hand:

  • Has discrete methods for checking if a given entity can be saved, updated or deleted, which receive as parameters the entity itself and a pointer to the DbContext to which the ruleset was applied;
  • Has a name property for helping us identifying what failed.

A ruleset really doesn’t need a public implementation, all we need is its interface. The private (internal) implementation might look like this:

   1: sealed class Ruleset : IRuleset
   2: {
   3:     private readonly IDictionary<Type, HashSet<Object>> rules = new Dictionary<Type, HashSet<Object>>();
   4:     private ObjectContext octx = null;
   5:  
   6:     internal Ruleset(ObjectContext octx)
   7:     {
   8:         this.octx = octx;
   9:     }
  10:  
  11:     public void AddRule<T>(IRule<T> rule)
  12:     {
  13:         if (this.rules.ContainsKey(typeof(T)) == false)
  14:         {
  15:             this.rules[typeof(T)] = new HashSet<Object>();
  16:         }
  17:  
  18:         this.rules[typeof(T)].Add(rule);
  19:     }
  20:  
  21:     public IEnumerable<IRule<T>> GetRules<T>()
  22:     {
  23:         if (this.rules.ContainsKey(typeof(T)) == true)
  24:         {
  25:             foreach (IRule<T> rule in this.rules[typeof(T)])
  26:             {
  27:                 yield return (rule);
  28:             }
  29:         }            
  30:     }
  31:  
  32:     public void Dispose()
  33:     {
  34:         this.octx.SavingChanges -= RulesExtensions.OnSaving;
  35:         RulesExtensions.rulesets.Remove(this.octx);
  36:         this.octx = null;
  37:  
  38:         this.rules.Clear();
  39:     }
  40: }

Basically, this implementation:

  • Stores the ObjectContext of the DbContext to which it was created for, this is so that later we can remove the association;
  • Has a collection - a set, actually, which does not allow duplication - of rules indexed by the real Type of an entity (because of proxying, an entity may be of a type that inherits from the class that we declared);
  • Has generic methods for adding and enumerating rules of a given type;
  • Has a Dispose method for cancelling the enforcement of the rules.

A (really dumb) rule applied to Product might look like this:

   1: class ProductRule : IRule<Product>
   2: {
   3:     #region IRule<Product> Members
   4:  
   5:     public String Name
   6:     {
   7:         get
   8:         {
   9:             return ("Rule 1");
  10:         }
  11:     }
  12:  
  13:     public Boolean CanSave(Product entity, DbContext ctx)
  14:     {
  15:         return (entity.Price > 10000);
  16:     }
  17:  
  18:     public Boolean CanUpdate(Product entity, DbContext ctx)
  19:     {
  20:         return (true);
  21:     }
  22:  
  23:     public Boolean CanDelete(Product entity, DbContext ctx)
  24:     {
  25:         return (true);
  26:     }
  27:  
  28:     #endregion
  29: }

The DbContext is there because we may need to check something else in the database before deciding whether to allow an operation or not.

And here’s how to apply this mechanism to any DbContext, without requiring the usage of a subclass, by means of an extension method:

   1: public static class RulesExtensions
   2: {
   3:     private static readonly MethodInfo getRulesMethod = typeof(IRuleset).GetMethod("GetRules");
   4:     internal static readonly IDictionary<ObjectContext, Tuple<IRuleset, DbContext>> rulesets = new Dictionary<ObjectContext, Tuple<IRuleset, DbContext>>();
   5:  
   6:     private static Type GetRealType(Object entity)
   7:     {
   8:         return (entity.GetType().Assembly.IsDynamic == true ? entity.GetType().BaseType : entity.GetType());
   9:     }
  10:  
  11:     internal static void OnSaving(Object sender, EventArgs e)
  12:     {
  13:         ObjectContext octx = sender as ObjectContext;
  14:         IRuleset ruleset = rulesets[octx].Item1;
  15:         DbContext ctx = rulesets[octx].Item2;
  16:  
  17:         foreach (ObjectStateEntry entry in octx.ObjectStateManager.GetObjectStateEntries(EntityState.Added))
  18:         {
  19:             Object entity = entry.Entity;
  20:             Type realType = GetRealType(entity);
  21:  
  22:             foreach (dynamic rule in (getRulesMethod.MakeGenericMethod(realType).Invoke(ruleset, null) as IEnumerable))
  23:             {
  24:                 if (rule.CanSave(entity, ctx) == false)
  25:                 {
  26:                     throw (new Exception(String.Format("Cannot save entity {0} due to rule {1}", entity, rule.Name)));
  27:                 }
  28:             }
  29:         }
  30:  
  31:         foreach (ObjectStateEntry entry in octx.ObjectStateManager.GetObjectStateEntries(EntityState.Deleted))
  32:         {
  33:             Object entity = entry.Entity;
  34:             Type realType = GetRealType(entity);
  35:  
  36:             foreach (dynamic rule in (getRulesMethod.MakeGenericMethod(realType).Invoke(ruleset, null) as IEnumerable))
  37:             {
  38:                 if (rule.CanDelete(entity, ctx) == false)
  39:                 {
  40:                     throw (new Exception(String.Format("Cannot delete entity {0} due to rule {1}", entity, rule.Name)));
  41:                 }
  42:             }
  43:         }
  44:  
  45:         foreach (ObjectStateEntry entry in octx.ObjectStateManager.GetObjectStateEntries(EntityState.Modified))
  46:         {
  47:             Object entity = entry.Entity;
  48:             Type realType = GetRealType(entity);
  49:  
  50:             foreach (dynamic rule in (getRulesMethod.MakeGenericMethod(realType).Invoke(ruleset, null) as IEnumerable))
  51:             {
  52:                 if (rule.CanUpdate(entity, ctx) == false)
  53:                 {
  54:                     throw (new Exception(String.Format("Cannot update entity {0} due to rule {1}", entity, rule.Name)));
  55:                 }
  56:             }
  57:         }
  58:     }
  59:  
  60:     public static IRuleset CreateRuleset(this DbContext context)
  61:     {
  62:         Tuple<IRuleset, DbContext> ruleset = null;
  63:         ObjectContext octx = (context as IObjectContextAdapter).ObjectContext;
  64:  
  65:         if (rulesets.TryGetValue(octx, out ruleset) == false)
  66:         {
  67:             ruleset = rulesets[octx] = new Tuple<IRuleset, DbContext>(new Ruleset(octx), context);
  68:             
  69:             octx.SavingChanges += OnSaving;
  70:         }
  71:  
  72:         return (ruleset.Item1);
  73:     }
  74: }

It relies on the SavingChanges event of the ObjectContext to intercept the saving operations before they are actually issued. Yes, it uses a bit of dynamic magic! Very handy, by the way! Winking smile

So, let’s put it all together:

   1: using (MyContext ctx = new MyContext())
   2: {
   3:     IRuleset rules = ctx.CreateRuleset();
   4:     rules.AddRule(new ProductRule());
   5:  
   6:     ctx.Products.Add(new Product() { Name = "xyz", Price = 50000 });
   7:  
   8:     ctx.SaveChanges();    //an exception is fired here
   9:  
  10:     //when we no longer need to apply the rules
  11:     rules.Dispose();
  12: }

Feel free to use it and extend it any way you like, and do give me your feedback!

As a final note, this can be easily changed to support plain old Entity Framework (not Code First, that is), if that is what you are using.

© ASP.net Weblogs or respective owner

Related posts about Entity Framework

Related posts about Entity Framework Code First