NHibernate Fluent Validation

Some time ago, I wrote a post on fluent validation for Entity Framework Code First. I think it is a cool concept, and I decided to bring it into NHibernate!

In a nutshell, what I want to be able to achieve is something like this:

   1: var validation = sessionFactory
   2:     .FluentlyValidate()
   3:     .Entity<SomeEntity>(x => x.SomeValue != "", "SomeValue is empty")
   4:     .Entity<AnotherEntity>(x => x.AnotherValue != 0, "AnotherValue is 0");

You can see that whenever I am about to save (update or insert) some entity of types SomeEntity or AnotherEntity, the fluent validation will occur, and an exception will be thrown if the entity does not pass. Fluent validation consists of one lambda expression – which can have several conditions – applied to an entity and returning a boolean result.

For this, I am using NHibernate’s listeners, and I am adding them dynamically through the ISessionFactory, this way it will apply to all ISessions spawned from it.

Without further delay, here is the code, first, the session factory extensions:

   1: public static class SessionFactoryExtensions
   2: {
   3:     internal static readonly IDictionary<GCHandle, FluentValidation> validations = new ConcurrentDictionary<GCHandle, FluentValidation>();
   4:  
   5:     public static FluentValidation FluentlyValidate(this ISessionFactory sessionFactory)
   6:     {
   7:         var validation = GetValidator(sessionFactory);
   8:  
   9:         if (validation == null)
  10:         {
  11:             validation = Register(sessionFactory);
  12:         }
  13:  
  14:         return (validation);
  15:     }
  16:  
  17:     public static void DisableFluentValidation(this ISessionFactory sessionFactory)
  18:     {
  19:         Unregister(sessionFactory);
  20:     }
  21:  
  22:     internal static FluentValidation GetValidator(this ISessionFactory sessionFactory)
  23:     {
  24:         return (validations.Where(x => x.Key.Target == sessionFactory).Select(x => x.Value).SingleOrDefault());
  25:     }
  26:  
  27:     private static void Unregister(ISessionFactory sessionFactory)
  28:     {
  29:         var validation = validations.Where(x => x.Key.Target == sessionFactory).SingleOrDefault();
  30:  
  31:         if (Object.Equals(validation, null) == true)
  32:         {
  33:             validations.Remove(validation);
  34:         }
  35:  
  36:         (sessionFactory as SessionFactoryImpl).EventListeners.FlushEntityEventListeners = (sessionFactory as SessionFactoryImpl).EventListeners.FlushEntityEventListeners.Where(x => !(x is FlushEntityValidatorListener)).ToArray();
  37:     }
  38:  
  39:     private static FluentValidation Register(ISessionFactory sessionFactory)
  40:     {
  41:         var validation = (validations[GCHandle.Alloc(sessionFactory)] = new FluentValidation());
  42:         (sessionFactory as SessionFactoryImpl).EventListeners.FlushEntityEventListeners = (sessionFactory as SessionFactoryImpl).EventListeners.FlushEntityEventListeners.Concat(new IFlushEntityEventListener[] { new FlushEntityValidatorListener() }).ToArray();
  43:         return (validation);
  44:     }
  45: }

This supports having multiple session factories, and because I am using a GCHandle to wrap them, they are not prevented from being garbage collected.

Next, the class that does the actual validation:

   1: public sealed class FluentValidation
   2: {
   3:     private readonly IDictionary<Type, Dictionary<Delegate, String>> conditions = new ConcurrentDictionary<Type, Dictionary<Delegate, String>>();
   4:     
   5:     public FluentValidation Clear<T>()
   6:     {
   7:         foreach (var type in this.conditions.Where(x => typeof(T).IsAssignableFrom(x.Key)).Select(x => x.Key))
   8:         {
   9:             this.conditions.Remove(type);
  10:         }
  11:         
  12:         return (this);
  13:     }
  14:  
  15:     public FluentValidation Entity<T>(Func<T, Boolean> condition, String message)
  16:     {
  17:         if (this.conditions.ContainsKey(typeof(T)) == false)
  18:         {
  19:             this.conditions[typeof(T)] = new Dictionary<Delegate, String>();
  20:         }
  21:  
  22:         this.conditions[typeof(T)][condition] = message;            
  23:  
  24:         return (this);
  25:     }
  26:  
  27:     public void Validate(Object entity)
  28:     {
  29:         var applicableConditions = this.conditions.Where(x => entity.GetType().IsAssignableFrom(x.Key)).Select(x => x.Value);
  30:  
  31:         foreach (var applicableCondition in applicableConditions)
  32:         {
  33:             foreach (var condition in applicableCondition)
  34:             {
  35:                 var del = condition.Key;
  36:  
  37:                 if (Object.Equals(del.DynamicInvoke(entity), false) == true)
  38:                 {
  39:                     throw (new ValidationException(entity, condition.Value));
  40:                 }
  41:             }
  42:         }
  43:     }
  44: }

This class holds a collection of validations for an entity type. If the validation fails, it throws an instance of a custom exception class:

   1: [Serializable]
   2: public sealed class ValidationException : Exception
   3: {
   4:     public ValidationException(Object entity, String message) : base(message)
   5:     {
   6:         this.Entity = entity;
   7:     }
   8:  
   9:     public Object Entity
  10:     {
  11:         get;
  12:         private set;
  13:     }        
  14: }

Finally, the NHibernate listener:

   1: sealed class FlushEntityValidatorListener : IFlushEntityEventListener
   2: {
   3:     #region IFlushEntityEventListener Members
   4:  
   5:     public void OnFlushEntity(FlushEntityEvent @event)
   6:     {
   7:         var validator = @event.Session.SessionFactory.GetValidator();
   8:  
   9:         if (validator != null)
  10:         {
  11:             validator.Validate(@event.Entity);
  12:         }
  13:     }
  14:  
  15:     #endregion
  16: }

When we no longer need fluent validation, we can disable it altogether:

   1: sessionFactory.DisableFluentValidation();

Or just for a particular entity:

   1: validation.Clear<SomeEntity>();

As always, hope you like it! Do send me your comments!

                             

No Comments

Add a Comment

As it will appear on the website

Not displayed

Your website