Login using System.Noticeably.Different.WebSite;
November 19, 2008 NoticeablyDifferent
Search for
 
Using Reflection To Validate Custom Instrumented Data Transfer (Value) Objects 
The goal of this sample is to create an implementation of data transfer (value) object validation that performs basic validation of the value of each of an object's properties. There was a requirement to validate the entire object and pass back a collection of validation messages rather than fail after the first violation.
 
The more practical reasoning for this sample is to perform validation on objects passed between a client and server. I wanted to enable some rudimentary validation to be transfered permitted on the value objects. Some may argue that this violated the definition of a data transfer object (or value object), but I would argue that this degree of validation can remain no less a violation than the data type declaration of the property but adds the significant benefit of reducing network calls as the client can perform this validation prior to making a network call.
 
Sample usage:
MyDataTransferObject dataTransferObject = new MyDataTransferObject();
try {
    Validator.Validate<MyDataTransferObject>(dataTransferObject);
} catch (ValidationException validationException) {
    /* Now you can loop through each validation violation
     * and do things like change the control that maps to ;
     * the given property to a different color and parse an
     * error message that shows all of the validation
     * violations that have occurred.
     */
    foreach(ValidationViolation violation in
        validationException.ValidationViolations) {
        // violation.Property;
        // violation.ViolationMessage;
    }
}
 
public class MyDataTransferObject {
    const string PostalCodeViolationMessage = "Postal code not valid";
    const string PostalCodeRegexPattern = @"[A-Z][0-9][A-Z]\s[0-9][A-Z][0-9]";
    const string RatingViolationMessage = "Rating must be between 1 and 10";
    const int RatingMaxValue = 10;
    const int RatingMinValue = 1;
 
    string postalCode = String.Empty;
    int rating;
 
    [RegexValidation(PostalCodeViolationMessage,
        PostalCodeRegexPattern, RegexOptions.None)]
    public string PostalCode {
        get { return postalCode; }
        set { postalCode = value; }
    }
 
    [IntegerValidation(RatingViolationMessage,
        RatingMaxValue, RatingMinValue)]
    public int Rating {
        get { return rating; }
        set { rating = value}
    }
}
 
 
The Code:
public sealed class Validator {
    static string ConstraintViolationHasOccurred = Resources.ObjectValidation.ValidationExceptionHasOccurred;
 
    public static void Validate<T>(T item) {
        if (item == null) throw new ArgumentNullException("item");
        IList<ValidationViolation> constraintViolations = new List<ValidationViolation>();
        foreach (PropertyInfo propertyInfo in item.GetType().GetProperties()) {
            foreach (ValidationAttribute validationCondition in
                propertyInfo.GetCustomAttributes(typeof(ValidationAttribute), true)) {
                    if (propertyInfo.CanRead && !validationCondition.IsValid(propertyInfo.GetValue(item, null))) {
                        constraintViolations.Add(new ValidationViolation(propertyInfo.Name, validationCondition.ViolationMessage));
                    }
                }
        }
        if (constraintViolations.Count > 0) {
            throw new ValidationException(ConstraintViolationHasOccurred, constraintViolations);
        }
    }
}
 
[Serializable]
public class ValidationViolation {
    string property, violationMessage;
 
    public ValidationViolation(string property, string violationMessage) {
        if (String.IsNullOrEmpty(property)) throw new ArgumentNullException("property");
        if (String.IsNullOrEmpty(violationMessage)) throw new ArgumentNullException("violationMessage");
        this.property = property;
        this.violationMessage = violationMessage;
    }
 
    public string Property {
        get { return property; }
    }
 
    public string ViolationMessage {
        get { return violationMessage; }
    }
}
 
public class ValidationException : Exception {
    IList<ValidationViolation> validationViolations = new List<ValidationViolation>();
 
    public ValidationException(string message, IList<ValidationViolation> validationViolations)
        : base(message) {
        if (validationViolations == null) throw new ArgumentNullException("validationViolations");
        this.validationViolations = validationViolations;
    }
 
    public IList<ValidationViolation> ValidationViolations {
        get { return validationViolations; }
    }
}
 
The ValidationAttribute class is a huge candidate for using generics but, unfortunately, the C# Language Specification (Section 20.5.8 of the March 2005 Draft) does not allow objects deriving from System.Attribute to implement generic types. If you try, it results in Compiler Error CS0698: A generic type cannot derive from 'class' because it is an attribute class.
 
This results in having to accept that the IsValid method will have an object parameter rather than a typed parameter, which would be preferable.
 
[AttributeUsage(AttributeTargets.Property)]
public abstract class ValidationAttribute : Attribute {
    string violationMessage;
 
    public ValidationAttribute(string violationMessage) {
        if (String.IsNullOrEmpty(violationMessage)) throw new ArgumentNullException("violationMessage");
        this.violationMessage = violationMessage;
    }
 
    public abstract bool IsValid(object value);
 
    public string ViolationMessage {
        get { return violationMessage; }
    }
}
 
[AttributeUsage(AttributeTargets.Property)]
public class RegexValidationAttribute : ValidationAttribute {
    Regex regularExpression;
 
    public RegexValidationAttribute(string violationMessage, string pattern)
        : base(violationMessage) {
        if (String.IsNullOrEmpty(violationMessage)) throw new ArgumentException("violationMessage");
        if (String.IsNullOrEmpty(pattern)) throw new ArgumentException("pattern");
        regularExpression = new Regex(pattern);
    }
 
    public RegexValidationAttribute(string violationMessage, string pattern, RegexOptions options)
        : base(violationMessage) {
        if (String.IsNullOrEmpty(violationMessage)) throw new ArgumentException("violationMessage");
        if (String.IsNullOrEmpty(pattern)) throw new ArgumentException("pattern");
        regularExpression = new Regex(pattern, options);
    }
 
    public override bool IsValid(object value) {
        if (!(value is string))
            throw new InvalidOperationException(
                Resources.ObjectValidation.RegexValidationAttributeOnlyAppliesToStringTypes);
        return regularExpression.Matches((stringvalue).Count == 1;
    }
}
 
[AttributeUsage(AttributeTargets.Property)]
public class IntegerValidationAttribute : ValidationAttribute {
    int maxValue, minValue;
 
    public IntegerValidationAttribute(string violationMessage, int maxValue, int minValue)
        : base(violationMessage) {
        if (minValue > maxValue)
            throw new ArgumentException(Resources.ObjectValidation.MinValueMustBeLessThanOrEqualToMaxValue);
 
        this.maxValue = maxValue;
        this.minValue = minValue;
    }
 
    public override bool IsValid(object value) {
        if (!(value is int))
            throw new InvalidOperationException(
                Resources.ObjectValidation.IntegerValidationAttributeOnlyAppliesToIntegerTypes);
        return (int)value <= maxValue && (int)value >= minValue;
    }
}