Usage of WhoCanHelpMe.Framework.Validation.IValidateMultipleProperties

Jan 6, 2010 at 11:52 PM

Hi All,

I'm interested in the following type:

WhoCanHelpMe.Framework.Validation.IValidateMultipleProperties

    /// <summary>
    /// Interface for a class level custom validator attribute 
    /// that validates based on more than one property. The
    /// primary property name is used to determine which of 
    /// the properties the validation error should refer to.
    /// </summary>
    public interface IValidateMultipleProperties : IRuleArgs
    {
        /// <summary>
        /// Gets PrimaryPropertyName.
        /// </summary>
        /// <value>
        /// The primary property name.
        /// </value>
        string PrimaryPropertyName
        {
            get;
        }
    }

Judging by the lack of usage, I would say that this type is a work in progress... and I think it sounds useful!

It's only usage (that I could find) is in WCHM.Framework.Validation.ValidatableExtensions:

        /// <summary>
        /// Returns an ErrorInfo for a class level validation result
        /// </summary>
        /// <param name="result">
        /// The validation result.
        /// </param>
        /// <returns>
        /// The ErrorInfo
        /// </returns>
        private static ErrorInfo GetClassLevelErrorInfo(IValidationResult result)
        {
            var errorInfo = new ErrorInfo(string.Empty, result.Message);

            // Get the validation attributes on the entity type
            var validatorProperties = result.ClassContext.GetCustomAttributes(false)
                .Where(x => typeof(IValidateMultipleProperties).IsAssignableFrom(x.GetType()));

            // If the validation message matches one of the attributes messages,
            // then set the correct property path, based on the primary property name
            validatorProperties.ForEach(x =>
                {
                    if (result.Message == ((IValidateMultipleProperties) x).Message)
                    {
                        errorInfo =
                            new ErrorInfo(
                                ((ValidationResult)result).InvalidValue.PropertyPath + ((IValidateMultipleProperties) x).PrimaryPropertyName,
                                result.Message);
                    }
                });

            return errorInfo;
        }
    }

Is this type meant to serve as some kind of class level validator?

As far as I understand NHibernate.Validator, it's great for property/field level validatition, but is not suited well for running business logic validation (ie, running a validation method on a class that compares two properties based on some business rule). Does this type try to migitate this?

If so, do you have an example usage that you can share?

Alternatively, if you have suggestions on other ways to do such business rule validation, please let me know!

Your input is greatly appreciated, as always.

Thank you!

Developer
Jan 7, 2010 at 10:19 AM

Hi there, 

Yes - this is intended as a class level validator. It probably shouldn't be in the solution as its not being used - remember WCHM started off as a cut down version of the architecture we used for FancyDressOutfitters.co.uk, so there may be a couple of things in there that aren't actually being used in WCHM! Sorry for the confusion. But, seeing as though you asked, here's an example of how it would be used....

Business scenario:

We have an Address entity that has regular expression postcode validation - except that postcodes are country specific, and we might not have a regular expression for each country. So, the validation of the postcode depends on the country value.

 

We start by creating the class level attribute, which inherits from IValidateMultipleProperties:

 

    [AttributeUsage(AttributeTargets.Class)]
    [ValidatorClass(typeof(CountrySpecificPostcodeValidator))]
    public class CountrySpecificPostcodeAttribute : Attribute, IValidateMultipleProperties
    {
        private readonly string countryCodeProperty;
        private readonly string postcodeProperty;

        public CountrySpecificPostcodeAttribute(string countryCodeProperty, string postcodeProperty)
        {
            this.countryCodeProperty = countryCodeProperty;
            this.postcodeProperty = postcodeProperty;
        }

        public string CountryCodeProperty
        {
            get { return this.countryCodeProperty; }
        }

        public string PostcodeProperty
        {
            get { return this.postcodeProperty; }
        }

        public string Message
        {
            get; set;
        }

        public string PrimaryPropertyName
        {
            get { return this.postcodeProperty; }
        }
    }

This is basically setting the property names that are relevant on the class, which is the primary property (the one that will show up as invalid - looking at this now, maybe I'd rename this to PropertyToValidate) and the validation message. It also has the ValidatorClass attribute which links it to the actual validation class - this attribute lives in the NHibernate.Validator.Engine namespace.

Then we have the class that does the actual validation:

 

 

    public class CountrySpecificPostcodeValidator : IInitializableValidator<CountrySpecificPostcodeAttribute>
    {
        private string countryCodeProperty;
        private string message;
        private string postcodeProperty;

        public void Initialize(CountrySpecificPostcodeAttribute parameters)
        {
            this.countryCodeProperty = parameters.CountryCodeProperty;
            this.postcodeProperty = parameters.PostcodeProperty;
            this.message = parameters.Message;
        }

        public bool IsValid(object value)
        {
            var countryCode = value.TryGetPropertyValue(this.countryCodeProperty);
            var postcode = value.TryGetPropertyValue(this.postcodeProperty);

            if (countryCode.IsNullOrEmpty())
            {
                return true; // Not testing that is is required, just that it is valid
            }

            if (ValidationRegExpPatterns.PostCodesByCountry.ContainsKey(countryCode))
            {
                return new Regex(ValidationRegExpPatterns.PostCodesByCountry[countryCode]).IsMatch(postcode);
            }

            return true; // No pattern for this country? Then accept anything.
        }
    }

This class inherits from the IInitializableValidator interface (also from the NHibernate.Validator.Engine namespace) which inherits from IValidator and tells us we need to provide the IsValid() method. In here we write our custom validation logic - in this case, retrieve both property values, check if we have a reg exp for the country code specified, and if so, validate the postcode against the pattern. 

 

The extension method TryGetPropertyValue exists as the IValidateMultipleProperties attribute is ultimately specifying the property names to validate as strings.

 

 

public static class ObjectExtensions
    {
        public static string TryGetPropertyValue(this object theObject, string propertyName)
        {
            if (theObject == null)
            {
                return null;
            }

            var propertyInfo = theObject.GetType().GetProperty(propertyName);
            if (propertyInfo == null)
            {
                throw new ArgumentException(
                                            string.Format(
                                            "Property with name {0} does not exist on class {1}",
                                            propertyName, 
                                            theObject.GetType().Name));
            }

            return (propertyInfo.GetValue(theObject, null) == null) ? null : propertyInfo.GetValue(theObject, null).ToString();
        }
    }
 

 

 

Finally, we can then apply the class level attribute to the Address entity:

 

 

    [CountrySpecificPostcode("CountryIsoCode", "Postcode", Message = "Must be a valid Postcode.")]
    public class Address : Entity
    {
        public virtual string CountryIsoCode
        {
            get; set;
        }

        public virtual string Postcode
        {
            get; set;
        }

        // more address properties...
    }

As you stated - when .Validate() is called on an Entity, the extension method in WCHM.Framework.Validation.ValidatableExtensions invokes the IsValid() method on all the validation rules and adds the errors to the collection - in this case, using the PrimaryPropertyName as the invalid property.

 

Hope all this makes sense - the approach is pretty standard for creating custom NHibernate validators, the only extra piece was using the IValidateMultipleProperties interface to be able to include calling the class level validation from within the Validate() extension method.

 

This is probably worth a proper blog post - but hopefully this should explain it for you.

 

Cheers

James

 

 

    [AttributeUsage(AttributeTargets.Class)]
    [ValidatorClass(typeof(CountrySpecificPostcodeValidator))]
    public class CountrySpecificPostcodeAttribute : Attribute, IValidateMultipleProperties
    {
        private readonly string countryCodeProperty;
        private readonly string postcodeProperty;
        public CountrySpecificPostcodeAttribute(string countryCodeProperty, string postcodeProperty)
        {
            this.countryCodeProperty = countryCodeProperty;
            this.postcodeProperty = postcodeProperty;
        }
        public string CountryCodeProperty
        {
            get { return this.countryCodeProperty; }
        }
        public string PostcodeProperty
        {
            get { return this.postcodeProperty; }
        }
        public string Message
        {
            get; set;
        }
        public string PrimaryPropertyName
        {
            get { return this.postcodeProperty; }
        }
    }

 

 

Jan 15, 2010 at 1:56 AM

Hi James,

Thank you very much for the detailed explanation and clarification.

 

1) In terms of namespace and folder structure, where do you put the CountrySpecificPostcodeValidator and CountrySpecificPostcodeAttribute objects?

 

2) In the last code snippet, you applied the CountrySpecificPostcode attribute to the Address entity. I notice that in WHCM, as far as the Domain layer is concerned, validation attributes are only defined on the "command object" entities (e.g. CreateProfileDetails) and not on the business entities themselves (e.g. Profile). Is this something you did with FancyDressOutfitters as well, or did you instead have your validation attributes on the business entities themselves, as the code example would lead me to believe?

 

 

Sorry for my late reply. It took me a while to get back to the validation topic and digest your feedback. Thank you again for your time!