65.9K
CodeProject is changing. Read more.
Home

Simple Attribute Based Validation

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.08/5 (8 votes)

Nov 27, 2007

3 min read

viewsIcon

76053

A quick introduction to reflection and attributes to allow for validation

Introduction

This quick introduction will provide the tools required to generate your own attribute based validation scheme. The methods presented here (though I have not had an opportunity to test them in production) seem to offer a powerful and simple method for validating objects. The original design was to validate against a database before allowing a save that would fail, however, in testing and tweaking I soon discovered that I could add more powerful business level validation attributes. I will leave it to the reader to imagine how best to implement this, however, I will note that there are speed increases that need to be made before moving to a production environment.

The Code

Below is a cut and paste of the code. I will go through the important aspects in more detail later:

using System;
using System.Data;
using System.Collections.Generic;
using System.Reflection;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;

/**
 * <summary>
 * This class provides attribute based validation
 * </summary>
 */
public class Validator {
    /**
     * <summary>
     * This method will validate the given method
     * </summary>
     * <remarks>
     * Validation will only work if the object
     * contains specific validation attributes
     * </remarks>
     * <returns>
     * A list of string values representing the errors
     * </returns>
     */
    public static IList<string> Validate(object o) {
        return Validate(o, false, "Object cannot be null");
    }//end Validate

    /**
     * <summary>
     * This method will validate the given method
     * </summary>
     * <remarks>
     * Validation will only work if the object
     * contains specific validation attributes
     * </remarks>
     * <returns>
     * A list of string values representing the errors
     * </returns>
     */
    public static IList<string> Validate
        (object o, bool allowNullObject, string nullMessage) {
        List<string> errors = new List<string>();
        if (o != null) {
            foreach (PropertyInfo info in o.GetType().GetProperties()) {
                foreach (object customAttribute in info.GetCustomAttributes
                    (typeof(IDbValidationAttribute), true)) {
                    ((IDbValidationAttribute)customAttribute).Validate
                        (o, info, errors);
                    if (info.PropertyType.IsClass || 
                        info.PropertyType.IsInterface) {
                        errors.AddRange((IList<string>)Validate
                            (info.GetValue(o, null), true, null));
                    }
                }
            }//end foreach

            foreach (MethodInfo method in o.GetType().GetMethods()) {
                foreach (object customAttribute in method.GetCustomAttributes
                        (typeof(IDbValidationAttribute), true)) {
                    ((IDbValidationAttribute)customAttribute).Validate
                        (o, method, errors);
                }
            }
        }
        else if (!allowNullObject) {
            errors.Add(nullMessage);
        }
        return errors;
    }
}
/**
 * <summary>
 * This interface provides validation signatures that can be called
 * based on the type of the attribute
 * </summary>
 * <remarks>
 * Ideally there should be a different interface for each kind of attribute
 * but this makes the code easier
 * </remarks>
 */
public interface IDbValidationAttribute {
    void Validate(object o, PropertyInfo propertyInfo, IList<string> errors);
    void Validate(object o, MethodInfo methodInfo, IList<string> errors);
}

[System.AttributeUsage(AttributeTargets.Method)]
public class CustomDatabaseValidationAttribute : 
    Attribute, IDbValidationAttribute {

    public void Validate(object o, PropertyInfo propertyInfo, 
        IList<string> errors) { }

    public void Validate(object o, MethodInfo info, IList<string> errors) {
        IList<string> result = (IList<string>)info.Invoke(o, null);
        foreach (string abc in result) {
            errors.Add(abc);
        }
    }
}

[System.AttributeUsage(AttributeTargets.Property)]
public class FieldNullableAttribute : Attribute, IDbValidationAttribute {
    private bool mIsNullable = false;
    private string mMessage = "{0} cannot be null";

    public bool IsNullable {
        get {
            return mIsNullable;
        }
        set {
            mIsNullable = value;
        }
    }

    public string Message {
        get {
            return mMessage;
        }
        set {
            if (value == null) {
                mMessage = String.Empty;
            }
            else {
                mMessage = value;
            }
        }
    }
    public void Validate(object o, MethodInfo info, 
        IList<string> errors) { }
    public void Validate(object o, PropertyInfo propertyInfo, 
        IList<string> errors) {
        object value = propertyInfo.GetValue(o, null);
        if (value == null && !IsNullable) {
            errors.Add(String.Format(mMessage, propertyInfo.Name));
        }
    }
}

[System.AttributeUsage(AttributeTargets.Property)]
public class FieldLengthAttribute : Attribute, IDbValidationAttribute {
    private int mMaxLegnth;
    private string mMessage = "{0} can only be {1} character(s) long";

    public int MaxLength {
        get {
            return mMaxLegnth;
        }
        set {
            mMaxLegnth = value;
        }
    }

    public string Message {
        get {
            return mMessage;
        }
        set {
            if (value == null) {
                mMessage = String.Empty;
            }
            else {
                mMessage = value;
            }
        }
    }
    public void Validate(object o, MethodInfo info, IList<string> errors) { }
    public void Validate(object o, PropertyInfo propertyInfo, 
        IList<string> errors) {
        object value = propertyInfo.GetValue(o, null);
        if (value is string) {
            if (MaxLength != 0 && ((string)value).Length >= MaxLength) {
                errors.Add(String.Format
                    (mMessage, propertyInfo.Name, MaxLength));
            }
        }
    }
}

First, you will notice on all of the attribute classes (they are the classes that inherit from System.Attribute) that there is an attribute usage attribute applied. Also you will notice that each attribute ends with the name Attribute - this is by convention. The attribute usage flags that are used in the code are:

[System.AttributeUsage(AttributeTargets.Property)]
[System.AttributeUsage(AttributeTargets.Method)]

In case it isn't clear, AttributeTargets.Property means the attribute can only be used on properties and AttributeTargets.Method can only be used on methods. There are numerous AttributeTargets and the best place for good documentation is still MSDN.

Next, notice that each Custom Attribute created has properties. While not necessary, they allow for named parameters when using the attributes. I know it is a little bit of syntax candy, but it is nice. Now we will look at one of the Validate methods:

public void Validate
    (object o, PropertyInfo propertyInfo, IList<string> errors) {
    object value = propertyInfo.GetValue(o, null);
    if (value is string) {
        if (MaxLength != 0 && ((string)value).Length >= MaxLength) {
        errors.Add(String.Format(mMessage, propertyInfo.Name, MaxLength));
        }
    }
}

Step one is to get the object's value and that is the first line of code. Although value is a reserved word in C#, it makes a lot of sense to use it here as the local variable, however, your boss will probably fire you for doing so. (Translation: At a job don't use value as a variable name.) Most of the related info classes in the System.Reflection namespace have a GetValue. The first argument is the instance of the object and the second value is an object array of parameters. If this were a method, I would use the Invoke method instead, which can be seen if you examine the relevant Validate method in the CustomDatabaseValidationAttribute class. If you are wondering where the PropertyInfo object came from, hold off and it will be addressed a little bit later.

errors.Add(String.Format(mMessage, propertyInfo.Name, MaxLength));

This is a poor man's way of tracking the errors. A more robust method would probably append actual exception objects. mMessage is a member tied to the Message property. This will allow the consumer of the attribute to provide a custom message (great for displaying a UI error to a user).

public interface IDbValidationAttribute {
    void Validate(object o, PropertyInfo propertyInfo, IList<string> errors);
    void Validate(object o, MethodInfo methodInfo, IList<string> errors);
}

I apologize for using this interface since not all of the Custom Attributes I have created need to implement both methods, although they code. The purpose for this interface was to provide an easier method for writing the code below:

public static IList<string> Validate
    (object o, bool allowNullObject, string nullMessage) {
    List<string> errors = new List<string>();
    if (o != null) {
        foreach (PropertyInfo info in o.GetType().GetProperties()) {
            foreach (object customAttribute in info.GetCustomAttributes
                (typeof(IDbValidationAttribute), true)) {
                ((IDbValidationAttribute)customAttribute).Validate
                    (o, info, errors);
                if (info.PropertyType.IsClass || 
                    info.PropertyType.IsInterface) {
                    errors.AddRange((IList<string>)Validate
                (info.GetValue(o, null), true, null));
                }
            }
        }//end foreach

        foreach (MethodInfo method in o.GetType().GetMethods()) {
            foreach (object customAttribute in method.GetCustomAttributes
                (typeof(IDbValidationAttribute), true)) {
                ((IDbValidationAttribute)customAttribute).Validate
                    (o, method, errors);
            }
        }
    }
    else if (!allowNullObject) {
        errors.Add(nullMessage);
    }
    return errors;
}

Instead of performing multiple type comparisons while iterating through the reflected properties and methods, the code can check for just one. This makes it a lot more readable. This code could also be reduced to only two for loops and a couple of if statements. However, the above is more readable which makes things nice when time comes to track down an error. Most of the code in this static Validate method stands on its own except for the recursive call:

if (info.PropertyType.IsClass || info.PropertyType.IsInterface) {
    errors.AddRange((IList<string>)Validate(info.GetValue
        (o, null), true, null));
}

This little bit of recursion prevents the requirement for an additional attribute to check properties that are objects. It is sloppy from a design perspective, again the choice here was for simple readability. Now that the code has been reviewed, I will present a simple usage scenario:

[Serializable]
public class Address {
    private Int64 mId;
    private string mLine1;
    private string mLine2;
    private string mCity;
    private string mState;
    private string mPostalCode;
    private string mCountry;
    private Address mOldAddress;

    [FieldNullable(IsNullable=true)]
    public Int64 Id {
        get {
            return mId;
        }
        set {
            mId = value;
        }
    }

    [FieldNullable(IsNullable=false)]
    [FieldLength(MaxLength = 50, Message="Address is a required field")]
    public string Line1 {
        get {
            return mLine1;
        }
        set {
            mLine1 = value;
        }
    }
    [FieldNullable(IsNullable = false)]
    [FieldLength(MaxLength = 50)]
    public string Line2 {
        get {
            return mLine2;
        }
        set {
            mLine2 = value;
        }
    }
    [FieldNullable(IsNullable = false)]
    [FieldLength(MaxLength = 10, Message="City Name is a required field")]
    public string City {
        get {
            return mCity;
        }
        set {
            mCity = value;
        }
    }
    [FieldNullable(IsNullable = false)]
    [FieldLength(MaxLength = 50, Message="State is a required field")]
    public string State {
        get {
            return mState;
        }
        set {
            mState = value;
        }
    }
    [FieldLength(MaxLength = 50, Message = "Postal Code is a required field")]
    public string PostalCode {
        get {
            return mPostalCode;
        }
        set {
            mPostalCode = value;
        }
    }
    [FieldNullable(IsNullable = false)]
    [FieldLength(MaxLength = 10)]
    public string Country {
        get {
            return mCountry;
        }
        set {
            mCountry = value;
        }
    }
    [FieldNullable(IsNullable = true)]=
    public string OldAddress {
        get {
            return mOldAddress;
        }
        set {
            mOldAddress = value;
        }
    }
}

The .NET compiler and intellisense are nice enough to make the FieldNullable class by chopping off the name Attribute. This makes it really convenient from both a usage and a design standpoint. It does this for all custom attributes.

Using the Code

usage:
Address address = new Address();
IList>string< errors = Validator.Validate(address);
if(errors.Count > 0) // there are errors!