Validation logic and NHibernate event listeners
Today, I’d like to discuss a particular case with validating input data using NHibernate event listeners.
The problem
In this post, Peter van Ooijen writes about an interesting example of using NHibernate event listeners. In short, he proposes to utilize them to create a single point in application where all validation logic takes place:
While such approach seems compelling, I’d like to raise a question: is it really an appropriate place to put the validation logic in?
There are several things ORMs are really good at. One of them (and the most important one) is hiding complexity that regards to object-relational mapping from the domain model. Ideally, ORM would allow us to create a clean separation of concerns in which domain classes and database wouldn’t interact with each other directly.
Separation of concerns also means that neither ORM nor database should contain any of the domain knowledge, to which the validation logic belongs.
Okay, so what’s exactly the problem with validation logic inside the NHibernate event listeners?
Issue #1: lack of explicitness
The first problem with this approach is the lack of explicitness. That is, if you follow DDD principles, you want your code base to reflect your domain as close and as explicitly as possible. The domain your software is aimed to solve problems in is the thing on which you should focus the most of your efforts.
If your domain contains some sophisticated validation rules (and even if they are rather simple), you need to express them in a lucid way. Not only should you clearly define the rules themselves, but also explicitly indicate where they are being invoked.
Putting invocation of those rules in the infrastructure layer, which NHibernate event listeners represent, leads to the growth of ambiguity and breaks the Separation of Concerns principle.
Issue #2: lack of invariant support
The second, and I guess the biggest problem here is the lack of invariant support. In most cases, you want your domain entities to be in a valid state during all their lifespan. That makes it extremely easy to follow the program flow as in every single point of your application, you are sure that the objects you operate don’t contain any invalid data and you don’t have to perform any checks on them. Check out my primitive obsession article to read more on this topic.
Creating non-static Validate method in domain classes assumes that they can stay in an invalid state. And that means domain objects don’t maintain their invariants. It also creates temporary coupling: you need to not forget to invoke Validate() before you want to use a domain object.
A better way of doing this is using guard clauses. They can protect a domain object’s state from being corrupted in the first place:
public class Customer : Entity
{
public string Name { get; private set; }
public int NumberOfEmployees { get; private set; }
public Customer(string name, int numberOfEmployees)
{
Contracts.Require(!string.IsNullOrWhiteSpace(name));
Contracts.Require(numberOfEmployees > 0);
Name = name;
NumberOfEmployees = numberOfEmployees;
}
}
That way you are always sure your domain objects keep their invariants and you can use them freely wherever you want.
Issue #3: invalid data inside the database
Another issue here is that this approach assumes data in the database can be invalid.
While it’s fine to have some checks on read operations that could validate data coming from the database, invalid data itself shouldn’t be treated as something acceptable. Your database resides in your "trusted boundary". Just like your domain objects shouldn’t contain any invalid data, your database shouldn’t contain it either.
What those checks could do is they could inform developers that there is something wrong with the data in the database. A DBA or developers could then transform this data throughout the database to make it valid.
But this is an exceptional case, and, of course, developers should try to keep data in database valid in the first place.
But what if your domain objects' invariants have changed? For example, your customers now must have at least 5 employees instead of one. The code itself is an easy part, you can just change the guard clause to reflect the new requirement:
Contracts.Require(numberOfEmployees > 5);
But what to do with the data?
Well, the data in the database needs to be migrated as well. Ask your product owner what to do with the existing customers that don’t have enough employees and create a migration script to implement this change. Your application’s database is a part of your software, so it needs to be changed every time your invariants change just like your code does.
So where to perform validation?
Okay, I think we are now clear that the approach described above is not the best way to deal with validation. But what is it then? Where to perform the validation?
I strongly believe that validation should take place at the borders of your domain. Your domain model, as well as your database, is inside your trusted boundary. That is, the boundary in which no invalid data can sneak, so you could freely make assumptions (supported by preconditions, preconditions, and invariants) about its validity.
If an invalid data is inside your domain model, it is simply too late to do anything. The best place to invoke the validation is application layer (e.g. controllers or presenters):
I wrote on a similar topic in my code contracts vs input validation article, so check it out to read more about input data validation.
What about valid use cases for NHibernate event listeners?
It is interesting to talk about valid use cases for NHibernate event listeners. I think they do a really great job in two situations:
-
When you need to add audition parameters to every domain object: who created or updated it and when.
-
When you need to fire your domain events. You can look at an example of it here.
I don’t claim this is an exhaustive list, but I can’t come up with any other use cases right now.
Summary
Let’s recap:
-
Domain objects should maintain their invariants.
-
Validation needs to be invoked explicitly at the domain model borders.
-
Database shouldn’t contain invalid data.
Subscribe
Comments
comments powered by Disqus