Another NHibernate Contribution project, NHibernate Validator, provides data validation for classes. In this recipe, I'll show you how to use NHibernate Validator attributes to validate your entities.
NHibernate.Validator.dll
, nhv-configuration.xsd
, and nhv-mapping.xsd
from the downloaded ZIP file to your solution's Lib
folder.xsd
files to the Schema
folder of your solution, just as we did with the NHibernate xml schema files in the Mapping a class with XML recipe in Chapter 1.Eg.Core
model and mappings from Chapter 1.Eg.AttributeValidation
.Eg.Core
model and mappings from Chapter 1 to this new project.Eg.AttributeValidation
.Eg.AttributeValidation
.Eg.AttributeValidation
project, add a reference to NHibernate.Validator
.NotNegativeDecimalAttribute
with the following code:[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] [Serializable] public class NotNegativeDecimalAttribute : DecimalMinAttribute { public NotNegativeDecimalAttribute() : base(0M) { } }
Product.cs
and add the following attributes:public class Product : Entity { [NotNull, Length(Min=1, Max=255)] public virtual string Name { get; set; } [NotNullNotEmpty] public virtual string Description { get; set; } [NotNull, NotNegativeDecimal] public virtual Decimal UnitPrice { get; set; } }
Eg.AttributeValidation.Runner
.Eg.AttributeValidation
model project, log4net.dll
, NHibernate.dll
, NHibernate.ByteCode.Castle.dll
, and NHibernate.Validator.dll
.App.config
with the standard log4net
and hibernate-configuration
sections, just as we did in the Configuring NHibernate with App.config and Configuring NHibernate Logging recipes of Chapter 2.<configSections>
element, add an additional section declaration named nhv-configuration
with the following xml:<section name="nhv-configuration" type="NHibernate.Validator.Cfg.ConfigurationSectionHandler, NHibernate.Validator" />
<nhv-configuration>
section with the following xml:<nhv-configuration xmlns="urn:nhv-configuration-1.0"> <property name='apply_to_ddl'>true</property> <property name='autoregister_listeners'>true</property> <property name='default_validator_mode'> OverrideExternalWithAttribute</property> <mapping assembly='Eg.AttributeValidation'/> </nhv-configuration>
BasicSharedEngineProvider
using the following code:public class BasicSharedEngineProvider : ISharedEngineProvider { private readonly ValidatorEngine ve; public BasicSharedEngineProvider(ValidatorEngine ve) { this.ve = ve; } public ValidatorEngine GetEngine() { return ve; } public void UseMe() { Environment.SharedEngineProvider = this; } }
Program.cs
, use the following code:class Program { static void Main(string[] args) { XmlConfigurator.Configure(); var log = LogManager.GetLogger(typeof(Program)); SetupNHibernateValidator(); var cfg = new Configuration().Configure(); cfg.Initialize(); var sessionFactory = cfg.BuildSessionFactory(); var schemaExport = new SchemaExport(cfg); schemaExport.Execute(true, true, false); var junk = new Product { Name = "Spiffy Junk", Description = "Stuff we can't sell.", UnitPrice = -1M }; using (var session = sessionFactory.OpenSession()) { using (var tx = session.BeginTransaction()) { try { session.Save(junk); tx.Commit(); } catch (InvalidStateException validationException) { var errors = validationException.GetInvalidValues(); foreach (var error in errors) log.ErrorFormat("Error with property {0}: {1}", error.PropertyName, error.Message); tx.Rollback(); } } } } private static ValidatorEngine GetValidatorEngine() { var validatorEngine = new ValidatorEngine(); validatorEngine.Configure(); return validatorEngine; } private static void SetupNHibernateValidator() { var validatorEngine = GetValidatorEngine(); new BasicSharedEngineProvider(validatorEngine).UseMe(); } }
NHibernate Validator, or NHV, has a few major components.
ValidatorEngine
is the main class used to interact with NHV. Like the session factory, applications typically have only one instance. NHV uses an implementation of ISharedEngineProvider
to find the singleton instance of the ValidatorEngine
. NHV may be used independently from NHibernate to validate any class. When integrated with NHibernate, it validates each entity before inserting or updating data. This integration is accomplished through ValidatePreInsertEventListener
and ValidatePreUpdateEventListener
event listeners.
To integrate NHV with NHibernate, we begin by creating a ValidatorEngine
. The call to ValidatorEngine.Configure()
loads our NHV configuration from the App.config
. Next, we create an ISharedEngineProvider
to return our ValidatorEngine
. We configure NHV to use this shared engine provider by setting the static property Environment.SharedEngineProvider
. Finally, after configuring NHibernate, but before creating the session factory, we call Initialize()
, an NHV extension method for the NHibernate configuration object.
Our NHV configuration in App.config
contains the following four configuration settings:
apply_to_ddl
: When this property is set to true, hbm2ddl
will generate database constraints to enforce many of our validation attributes. For example, the script to create our UnitPrice
column, shown next, now has a check constraint to enforce our NotNegativeDecimal
rule.UnitPrice DECIMAL(19,5) not null check( UnitPrice>=0)
autoregister_listeners
: This property determines if the Initialize
extension method will add the pre-insert
and pre-update
event listeners to the NHibernate configuration.default_validator_mode
: This property determines the priority of validation rules when using a mix of XML validation definitions, validation classes, or attributes.In this recipe, we attempt to save a new product with a negative UnitPrice
. This violates our NotNegativeDecimal
validation rule. Without NHibernate Validator, our application would silently accept the invalid data, leading to potentially larger problems later. If we had simply added a constraint in the database, our application would attempt to insert the bad data, then throw an unwieldy SQLException
that gives us no information about which property is invalid and why. With NHibernate Validator, the event listeners validate each entity before any data is written to the database. If they find invalid data, they throw an InvalidStateException
that tells us exactly which properties of the entity are invalid and why.
You may be wondering why we created a separate NotNegativeDecimalAttribute
class. Couldn't we just decorate our UnitPrice
property with [DecimalMin(0M)]
? As it turns out, we can't do this. In C#, we can't use decimal
parameters in this way. To work around this limitation, we subclass the DecimalMinAttribute
and hardcode the zero inside NotNegativeDecimalAttribute
class.
In our assemblies, attribute decorations are not stored as Intermediate Language (IL) instructions, but as metadata. This limits the types we can use as parameters. The C# specification at http://msdn.microsoft.com/en-us/library/aa664615(v=VS.71).aspx defines the types we can use as bool
, byte
, char
, double
, float
, int
, long
, short
, string
, object
, System.Type
, enum
, or any one-dimensional array of these types. decimal
is not on the list.
If you check your entities for invalid data prior to saving them, you don't run the risk of blowing up the NHibernate session. To validate an object explicitly, your code might look like this:
var ve = Environment.SharedEngineProvider.GetEngine(); var invalidValues = ve.Validate(someObject);
invalidValues
is an array of InvalidValue
objects describing each failed validation rule. If it's empty, the object is valid. If not, you can easily display the validation messages to the user without risking the session.
NHibernate Validator can be used to validate any class, not just NHibernate entities. You can easily adapt this sort of explicit validation to integrate with ASP.NET MVC's model validation.