NHibernate Pitfalls: Integrating NHibernate Validator

This is part of a series of posts about NHibernate Pitfalls. See the entire collection here.

NHibernate Validator is the standard way to implement custom validation of entities – that is, validation other than just based on the table schema definition. It is available from Nuget (https://nuget.org/packages/NHibernate.Validator) and as source code from GitHub (https://github.com/darioquintana/NHibernate-Validator).

This post is not about implementing custom validation. For that, I recommend the excellent collection of posts by Fabio Maulo and Dario Quintana.

This is about the actual integration of Validator with NHibernate; it has some gotchas that you should be aware of. Here’s a simple guide.

Let’s suppose you have a User class:

   1: public class User
   2: {
   3:     public virtual Int32 Id { get; protected set; }
   4:     public virtual String Name { get; set; }
   5:     public virtual String Email { get; set; }
   6:     public virtual DateTime Birthday { get; set; }
   7: }

You want to be sure that:

  • The Name and Email properties have values and have at most 50 characters;
  • The Email property is a valid email address;
  • The Birthday is in the past.

Let’s forget about the mappings, they should be very easy to create. Let’s focus instead on NHibernate Validator, and, following what seems the current trend on .NET, loquacious (or fluent) configuration:

   1: FluentConfiguration validatorConfiguration = new FluentConfiguration();
   2: ValidationDef<User> userValidation = new ValidationDef<User>();
   3: userValidation.Define(x => x.Birthday).IsInThePast().WithMessage("The birthday must be in the past")
   4: .And
   5: .Define(x => x.Email).NotNullableAndNotEmpty().WithMessage("The email is mandatory")
   6: .And
   7: .MaxLength(50).WithMessage("The email can only have 50 characters")
   8: .And
   9: .IsEmail().WithMessage("The email must be a valid email adddress")
  10: .And
  11: .Define(x => x.Name).NotNullableAndNotEmpty().WithMessage("The name is mandatory")
  12: .And
  13: .MaxLength(50).WithMessage("The email can only have 50 characters");
  15: ;

You can see that be created a validation configuration instance and we populated it with a validation definition for the User class. You could as well create your own class that inherits from ValidationDef<User> and place all validation logic there.

Next, we need to integrate NHibernate Validator with NHibernate:

   1: validationConfiguration.SetDefaultValidatorMode(ValidatorMode.UseExternal)
   2: .IntegrateWithNHibernate
   3: .ApplyingDDLConstraints()
   4: .RegisteringListeners();
   6: NHibernate.Validator.Cfg.Environment.SharedEngineProvider = new NHibernateSharedEngineProvider();
   7: ValidatorEngine validatorEngine = NHibernate.Validator.Cfg.Environment.SharedEngineProvider.GetEngine();
   8: validatorEngine.Configure(validatorConfiguration);            
  10: //cfg is the NHibernate Configuration instance
  11: cfg.Initialize(validatorEngine);

If you are curious, NHibernate Validator will add listeners for the PreInsert and PreUpdate events, which will be fired when the session is about to save or update some entity.

And that’s it! Now, validation will occur in two distinct ways:

  • Explicit, if you call ValidatorEngine.Validate:
   1: User u = new User();
   2: InvalidValue[] invalidValuesObtainedExplicitly = validatorEngine.Validate(u);
  • Implicit, if you try to save an invalid entity:
   1: ISession session = ...;
   3: try
   4: {
   5:     User u = new User();
   6:     session.Save(u);
   7:     session.Flush();
   8: }
   9: catch (InvalidStateException ex)
  10: {
  11:     InvalidValue [] invalidValuesObtainedFromException = ex.GetInvalidValues();
  12: }

Using a shared engine provider (the NHibernate.Validator.Cfg.Environment.SharedEngineProvider instance) is required, because the listeners have no other way of getting your configured ValidatorEngine. You are free – and advised to, actually – to create your own provider, by implementing NHibernate.Validator.Event.ISharedEngineProvider in the way that best suits your needs, perhaps using IoC to retrieve a single instance.


No Comments