Search code examples
validation.net-3.5asp.net-mvc-2data-annotations

Custom model validation of dependent properties using Data Annotations


Since now I've used the excellent FluentValidation library to validate my model classes. In web applications I use it in conjunction with the jquery.validate plugin to perform client side validation as well. One drawback is that much of the validation logic is repeated on the client side and is no longer centralized at a single place.

For this reason I'm looking for an alternative. There are many examples out there showing the usage of data annotations to perform model validation. It looks very promising. One thing I couldn't find out is how to validate a property that depends on another property value.

Let's take for example the following model:

public class Event
{
    [Required]
    public DateTime? StartDate { get; set; }
    [Required]
    public DateTime? EndDate { get; set; }
}

I would like to ensure that EndDate is greater than StartDate. I could write a custom validation attribute extending ValidationAttribute in order to perform custom validation logic. Unfortunately I couldn't find a way to obtain the model instance:

public class CustomValidationAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        // value represents the property value on which this attribute is applied
        // but how to obtain the object instance to which this property belongs?
        return true;
    }
}

I found that the CustomValidationAttribute seems to do the job because it has this ValidationContext property that contains the object instance being validated. Unfortunately this attribute has been added only in .NET 4.0. So my question is: can I achieve the same functionality in .NET 3.5 SP1?


UPDATE:

It seems that FluentValidation already supports clientside validation and metadata in ASP.NET MVC 2.

Still it would be good to know though if data annotations could be used to validate dependent properties.


Solution

  • MVC2 comes with a sample "PropertiesMustMatchAttribute" that shows how to get DataAnnotations to work for you and it should work in both .NET 3.5 and .NET 4.0. That sample code looks like this:

    [AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
    public sealed class PropertiesMustMatchAttribute : ValidationAttribute
    {
        private const string _defaultErrorMessage = "'{0}' and '{1}' do not match.";
    
        private readonly object _typeId = new object();
    
        public PropertiesMustMatchAttribute(string originalProperty, string confirmProperty)
            : base(_defaultErrorMessage)
        {
            OriginalProperty = originalProperty;
            ConfirmProperty = confirmProperty;
        }
    
        public string ConfirmProperty
        {
            get;
            private set;
        }
    
        public string OriginalProperty
        {
            get;
            private set;
        }
    
        public override object TypeId
        {
            get
            {
                return _typeId;
            }
        }
    
        public override string FormatErrorMessage(string name)
        {
            return String.Format(CultureInfo.CurrentUICulture, ErrorMessageString,
                OriginalProperty, ConfirmProperty);
        }
    
        public override bool IsValid(object value)
        {
            PropertyDescriptorCollection properties = TypeDescriptor.GetProperties(value);
            object originalValue = properties.Find(OriginalProperty, true /* ignoreCase */).GetValue(value);
            object confirmValue = properties.Find(ConfirmProperty, true /* ignoreCase */).GetValue(value);
            return Object.Equals(originalValue, confirmValue);
        }
    }
    

    When you use that attribute, rather than put it on a property of your model class, you put it on the class itself:

    [PropertiesMustMatch("NewPassword", "ConfirmPassword", ErrorMessage = "The new password and confirmation password do not match.")]
    public class ChangePasswordModel
    {
        public string NewPassword { get; set; }
        public string ConfirmPassword { get; set; }
    }
    

    When "IsValid" gets called on your custom attribute, the whole model instance is passed to it so you can get the dependent property values that way. You could easily follow this pattern to create a date comparison attribute, or even a more general comparison attribute.

    Brad Wilson has a good example on his blog showing how to add the client-side portion of the validation as well, though I'm not sure if that example will work in both .NET 3.5 and .NET 4.0.