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 logic4: 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 changes3: 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 : IDocumentStoreListener2: {
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
exists same easy way to use nhibernate validator?
And there is also the question of whether "validation by exception" is a proper way of doing validation...
@ugo Yes, check out IPreInsertEventListener and IPreUpdateEventListener.
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.
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?
This works fine. The interface is just to ensure that objects that do not need validation are skipped.
Beware, this is a 1 minute experiment.
David: The exception is thrown at SaveChanges(), so nothing is stored.
The example code is for Data Annotations, and the validated class is:
Ugo, Yes
Rangoric, Certainly, if you noted, I added code to handle typing explicitly.
David, If an exception is raised from the BeforeStore method, SaveChanges will NOT happen.
store.RegisterListener(validationListener); ???
I have no such RegisterListener. Maybe this is for a newer version? My version is 960
Uwe, It is there, it isn't on the IDocumentStore interface, but it is on the DocumentStore impl.
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?
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.
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?
Daniel, What do you need a RestoreState feature? Just don't save the diner
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)
Comment preview