Click here to Skip to main content
15,886,199 members
Articles / Programming Languages / C#
Article

Simple Attribute Based Validation

Rate me:
Please Sign up or sign in to vote.
4.08/5 (10 votes)
27 Nov 20073 min read 75.2K   46   10
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:

C#
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:

C#
[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:

C#
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.

C#
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).

C#
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:

C#
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:

C#
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:

C#
[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

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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Architect ERL GLOBAL, INC
United States United States
My company is ERL GLOBAL, INC. I develop Custom Programming solutions for business of all sizes. I also do Android Programming as I find it a refreshing break from the MS.

Comments and Discussions

 
Generalcomment on performance Pin
brian_agnes10-Dec-07 6:37
brian_agnes10-Dec-07 6:37 
GeneralRe: comment on performance Pin
Ennis Ray Lynch, Jr.14-Dec-07 3:12
Ennis Ray Lynch, Jr.14-Dec-07 3:12 
Questionhow download it? Pin
fabi____28-Nov-07 1:44
fabi____28-Nov-07 1:44 
AnswerCopy and paste Pin
Ennis Ray Lynch, Jr.28-Nov-07 1:57
Ennis Ray Lynch, Jr.28-Nov-07 1:57 
GeneralRe: Copy and paste Pin
fabi____28-Nov-07 2:27
fabi____28-Nov-07 2:27 
GeneralVery nice idea! some sugestion Pin
fabi____28-Nov-07 0:25
fabi____28-Nov-07 0:25 
GeneralOn my own side Pin
Ennis Ray Lynch, Jr.28-Nov-07 1:54
Ennis Ray Lynch, Jr.28-Nov-07 1:54 
GeneralGood article but exist another arcticles Pin
DimitrySTD27-Nov-07 21:34
DimitrySTD27-Nov-07 21:34 
GeneralThere are litterally thousands of articles Pin
Ennis Ray Lynch, Jr.28-Nov-07 1:59
Ennis Ray Lynch, Jr.28-Nov-07 1:59 
GeneralRe: Good article but exist another arcticles Pin
dimzon28-Nov-07 2:53
dimzon28-Nov-07 2:53 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.