Handling entities validations in RavenDB

time to read 15 min | 2926 words

This post came out of a stack overflow question. The user had the following code:

   1: public void StoreUser(User user)
   2: {
   3:     //Some validation logic
   4:     if(string.IsNullOrWhiteSpace(user.Name))
   5:         throw new Exception("User name can not be empty");
   6:  
   7:     Session.Store(user);
   8: }

But he noted that this will not work for other approaches, such as this:

   1: var u1 = Sesion.Load<User>(1);
   2: u1.Name = null; //change is tracked and will persist on the next save changes
   3: Session.SaveChanges();

This is because RavenDB tracks the entity and will persist it if there has been any changes when SaveChanges is called.

The question was:

Is there someway to get RavenDB to store only a snapshot of the item that was stored and not track further changes?

The answer is, as is often the case if you run into hardship with RavenDB, you are doing something wrong. In this particular case, that wrongness is the fact that you are trying to do validation manually. This means that you always have to remember to call it, and that you can’t use a lot of the good stuff that RavenDB gives you, like change tracking. Instead, RavenDB contains the hooks to do it once, and do it well.

   1: public class ValidationListener : IDocumentStoreListener
   2: {
   3:     readonly Dictionary<Type, List<Action<object>>> validations = new Dictionary<Type, List<Action<object>>>();
   4:  
   5:     public void Register<T>(Action<T> validate)
   6:     {
   7:         List<Action<object>> list;
   8:         if(validations.TryGetValue(typeof(T),out list) == false)
   9:             validations[typeof (T)] = list = new List<Action<object>>();
  10:  
  11:         list.Add(o => validate((T) o));
  12:     }
  13:  
  14:     public bool BeforeStore(string key, object entityInstance, RavenJObject metadata, RavenJObject original)
  15:     {
  16:         List<Action<object>> list;
  17:         if (validations.TryGetValue(entityInstance.GetType(), out list))
  18:         {
  19:             foreach (var validation in list)
  20:             {
  21:                 validation(entityInstance);
  22:             }
  23:         }
  24:         return false;
  25:     }
  26:  
  27:     public void AfterStore(string key, object entityInstance, RavenJObject metadata)
  28:     {
  29:     }
  30: }

This will be called by RavenDB whenever we save to the database. We can now write the validation / registration code like this:

   1: var validationListener = new ValidationListener();
   2: validationListener.Register<User>(user=>
   3:     {
   4:         if (string.IsNullOrWhiteSpace(user.Name))
   5:             throw new Exception("User name can not be empty");
   6:     });
   7: store.RegisterListener(validationListener);

And that is all that she wrote.