Property validation with attributes

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.

Getting ready

  1. Download the NHibernate Validator binary files from SourceForge at http://sourceforge.net/projects/nhcontrib/files/.
  2. Extract NHibernate.Validator.dll, nhv-configuration.xsd, and nhv-mapping.xsd from the downloaded ZIP file to your solution's Lib folder.
  3. Add both 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.
  4. Complete the Eg.Core model and mappings from Chapter 1.

How to do it...

  1. Create a new class library project named Eg.AttributeValidation.
  2. Copy the Eg.Core model and mappings from Chapter 1 to this new project.
  3. Change the namespace and assembly references in the mappings to Eg.AttributeValidation.
  4. Change the namespaces for the entity classes to Eg.AttributeValidation.
  5. In your Eg.AttributeValidation project, add a reference to NHibernate.Validator.
  6. Create a new attribute class named NotNegativeDecimalAttribute with the following code:
    [AttributeUsage(AttributeTargets.Field | 
      AttributeTargets.Property)]
    [Serializable]
    public class NotNegativeDecimalAttribute 
      : DecimalMinAttribute
    {
    
      public NotNegativeDecimalAttribute()
        : base(0M)
      {
      }
    
    }
  7. Open 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; }
    
    }
  8. Create a new console project named Eg.AttributeValidation.Runner.
  9. Add references to the Eg.AttributeValidation model project, log4net.dll, NHibernate.dll, NHibernate.ByteCode.Castle.dll, and NHibernate.Validator.dll.
  10. Set up an 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.
  11. In the <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" />
  12. Add the <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>
  13. Add a new class named 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;
      }
    
    }
  14. In 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();
      }
    
    }
  15. Build and run your program.

How it works...

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.
  • The NHV mapping element behaves similar to the NHibernate mapping element. It defines an assembly containing our entities decorated with 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.

Note

When a validation event listener throws an InvalidStateException, the session is in an undefined state. Once this happens, the only operation that can be safely performed on the session is Dispose.

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.

There's more...

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.

See also

  • Creating validator classes
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset