Ayende @ Rahien

Refunds available at head office

Handling entities validations in RavenDB

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.

Comments

ugo
10/10/2012 10:22 AM by
ugo

exists same easy way to use nhibernate validator?

Apostol
10/10/2012 11:44 AM by
Apostol

And there is also the question of whether "validation by exception" is a proper way of doing validation...

Alexander Nyquist
10/10/2012 12:14 PM by
Alexander Nyquist

@ugo Yes, check out IPreInsertEventListener and IPreUpdateEventListener.

Rangoric
10/10/2012 01:20 PM by
Rangoric

Oh there goes another chunk of code. I've been manually validating things with Data Annotations, this lets me get rid of that step altogether.

However, is it possible to register a generic validator? I have a single Validator already that takes any object and checks the data annotations.

David Boike
10/10/2012 02:15 PM by
David Boike

This is great! I've finished a RavenDB project and had no idea this feature existed!

It does raise one question for me though.

I could see implementing a custom user validation exception that would store all the different issues with a model, so you could easily catch that exception and translate it to a ValidationSummary type of control in a web application.

However, if you're using a one session per request model, and you trap the exception, the SaveChanges() is still going to occur. How do you deal with that? Does the Listener just make it so that the update to that model never took place, but all the other activity on the pages proceeds normally?

Duckie
10/10/2012 08:33 PM by
Duckie

This works fine. The interface is just to ensure that objects that do not need validation are skipped.

public interface IPersistValidated
{}

public class ValidationListener : IDocumentStoreListener
{
    public bool BeforeStore(string key, object entityInstance, RavenJObject metadata, RavenJObject original)
    {
        if (entityInstance is IPersistValidated)
        {
            var context = new ValidationContext(entityInstance, null, null);

            Validator.ValidateObject(entityInstance, context);
        }
        return true;
    }

    public void AfterStore(string key, object entityInstance, RavenJObject metadata)
    {
    }
}

Beware, this is a 1 minute experiment.

Duckie
10/10/2012 08:38 PM by
Duckie

David: The exception is thrown at SaveChanges(), so nothing is stored.

Duckie
10/10/2012 08:40 PM by
Duckie

The example code is for Data Annotations, and the validated class is:

public class User : IPersistValidated
{
    [Required]
    public string Username { get; set; }
    [Required]
    public PasswordString Password { get; set; } // just class so manage hashing
}
Ayende Rahien
10/11/2012 12:11 AM by
Ayende Rahien

Rangoric, Certainly, if you noted, I added code to handle typing explicitly.

Ayende Rahien
10/11/2012 12:12 AM by
Ayende Rahien

David, If an exception is raised from the BeforeStore method, SaveChanges will NOT happen.

Uwe
11/06/2012 09:00 AM by
Uwe

store.RegisterListener(validationListener); ???

I have no such RegisterListener. Maybe this is for a newer version? My version is 960

Ayende Rahien
11/06/2012 09:11 AM by
Ayende Rahien

Uwe, It is there, it isn't on the IDocumentStore interface, but it is on the DocumentStore impl.

Daniel
11/09/2012 12:57 AM by
Daniel

But there are other types of validations that involves other object instances at some the execution context. Let's suppose that in a Nerd Dinner like app, you cannot schedule ther same dinner twice.

In this case, we cannot "register" the validation, because it's in a business service.

Thoughts?

Ayende Rahien
11/09/2012 02:16 AM by
Ayende Rahien

Daniel, There is generally a distinction between business style validation (you can't order more than 5 items unless you have a Gold membership, or you can't schedule an event twice) vs. data validation (field is not null, age is valid, etc). This post talks about data validation, not business validation.

Daniel
11/09/2012 09:42 AM by
Daniel

Sure. Post a comment here because didn't find an approach to the business validation context.

In the case that a existing Dinner is edited and then validated before SaveChanges, I think that we could have a "RestoreState" feature (something like memento pattern).

Don't you think that could be useful?

Ayende Rahien
11/10/2012 10:17 AM by
Ayende Rahien

Daniel, What do you need a RestoreState feature? Just don't save the diner

Daniel
11/10/2012 11:15 AM by
Daniel

The dinner was loaded from database and edited, but some business validation over it throws exception. This exception is handled at the Application Level, and because of this, the end of the request calls de SaveChanges. I used the memento pattern to restore the state of the entity at the context of the exception handling to prevent an update at the SaveChanges. Maybe there is a better way to handle this scenario. For example, using a TransactionScope around this operation. In this case, RavenDB could have a VoteCommit and VoteRollback to control SaveChanges (similar to ActiveRecord.TransactionScope)

Comments have been closed on this topic.