Simple Attribute Based Validation






4.08/5 (8 votes)
Nov 27, 2007
3 min read

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 Attribute
s 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!