1. Introduction
This article shows how you can use attributes to validate your business objects. You may have had to write tones of code to validate your business objects without taking advantage of attributes. The value of attributes comes when you need to apply the same rules on a number of properties, and the only thing you have to do is to decorate your property accordingly. For other articles on attribute based validation, please check the Visual Studio Magazine and Simple Attribute Based Validation.
2. Background
The .NET compiler enables us to richly embed metadata into an assembly, which can be accessed at a later time by using Reflection. Attributes are decorative clauses that can be placed on classes, interfaces, methods, assemblies, modules, or other items. They become part of the assembly metadata, and can be used to assign rules to the compiler or to enable developers to reuse code to perform various operations including validation, tracing, and type conversion. Attributes are inherited from the System.Attribute
class.
Examples of attributes:
[Serializable, XmlRoot(Namespace = "www.idi.ac.ug")]
public class Person : EntityObject {
[XmlAttribute]
public string Firstname {get ; set;}
[XmlAttribute]
public string Lastname {get ; set;}
}
Some attributes that are commonly used include:
[Obsolete]
Tells compiler to issue a warning because the decorated method is obsolete.
[Serializable]
Tells compiler that the object can be serialized to some storage, as XML, text, or binary.
[Assembly: ]
These are assembly level attributes that are applied on the entire assembly.
[DefaultValue]
This is used to give a default value.
In our case, we would like to see how we can build our own custom attributes which we can use to make business object validation easier. The usability of attributes would surely save you time, code, and the stress of having to individually validate each property of a business object.
3. The business object
Consider a business object called Person
, which we have decided inherits from the base class EntityObject
. We have made the base class to implement the IEntityObject
interface although it in not vital for this article. The attributes used here are [Required]
, [InRange]
, and [DefaultValue]
.
public class Person : EntityObject
{
[DefaultValue("Mr")]
public string Title { get; set; }
[Required]
public string FirstName { get; set; }
[Required(ErrorMessage = "LastName must have a value")]
public string LastName { get; set; }
[InRange(18, 95)]
[DefaultValue(30)]
public int? Age { get; set; }
public override void Validate(object sender, ValidateEventArgs e)
{
base.Validate(sender, e);
if (this.Age == 25)
{ Errors.Add(new Error(this, "Person",
"A person cannot be 25 years of age!")); }
}
}
In the calling method, I would like to instantiate the Person
object and ensure that on object.Save()
method, which is provided by the base class, I validate the object. There are other approaches you may use, e.g., immediate validation when a property has been changed. I have decided to defer validation to the end because the process involves Reflection, which can be expensive.
Person person = new Person();
person.Firstname = "John";
person.Lastname = "Doe";
person.Age = 15;
string errMsg = "Could not save Person!\n\n";
if (!person.Save())
{
foreach(Error err in person.Errors)
{
errMsg += err.Message + "\n";
}
MessageBox.Show(errMsg, "Error", MessageBoxButtons.OK,
MessageBoxIcon.Exclamation);
}
else
{
MessageBox.Show("Person = Valid");
}
4. The attributes
Let’s have a look at the attribute classes presented below. We will begin with the [Required]
attribute, and I should quickly ask you to note that the actual class name is RequiredAttribute
, and .NET lets you write it nicely as [Required]
instead of [RequiredAttribute]
although it will still work.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class RequiredAttribute : System.Attribute
{
private bool required;
private string errorMessage;
public bool Required
{
get
{
return required;
}
set
{
required = value;
}
}
public string ErrorMessage
{
get
{
return errorMessage;
}
set
{
errorMessage = value;
}
}
public RequiredAttribute()
{
required = true;
}
public RequiredAttribute(bool required)
{
this.required = required;
}
public RequiredAttribute(string errorMessage)
{
this.errorMessage = errorMessage;
}
}
In the above Required
attribute, the [AttributeUsage]
is an attribute of an attribute - interesting, isn't it? It enables you to determine or restrict where the attribute may be used, and in my example above, we can only use the Required
attribute on properties. The AttributeTarget
is an enum in which you can choose the scope of where this attribute can be used. The default is All
; however, it could be Assembly
, Class
, Constructor
, Delegate
, Enum
, Event
, Field
, Interface
, Method
, Parameter
, Property
, ReturnValue
, or struct
.
You can use the AllowMultiple
parameter of the AttributeUsage
to determine whether the same attribute can be used more than once on the same target. In this case, we can have the [Required]
attribute only once on a property.
Attributes can only have constants.
5. The Business Object Base
I decided to place a public Errors
collection which will keep the errors encountered during validation. I also added event handlers that can be triggered when the validation method is executed. This enables hijacking the validation process if necessary, and inject validation rules after the object has been created. The Validate()
method is virtual, and therefore can be overridden to allow custom business rules. This is what you would use to ensure that a male person would not have a boolean Pregnant
property set to true
.
public class EntityObject : IEntityObject
{
#region Internal Fields
public List<error> Errors = new List<error>();
#endregion
#region Delegate and Events
public delegate void OnValidateEventHandler(object sender, ValidateEventArgs e);
public delegate void OnValidatedEventHandler(object sender, ValidateEventArgs e);
public OnValidateEventHandler OnValidate;
public OnValidatedEventHandler OnValidated;
.....
}
6. Reflection
We use Reflection, and the Validate
method loops through all the properties looking for associated custom attributes. We then test the property value against the attribute rule, and if it violates the rule, we then add an error into the Errors
collection.
The magic of knowing all the properties lies in the System.Reflection
namespace. We will then use PropertyInfo
to keep all the properties of the object and the GetType.GetProperties()
method as follows.
PropertyInfo info = this.GetType().GetProperties();
We furthermore check the attributes on each property to see if it matches, e.g., RequiredAttribute
; if it does, we then check the property value for violation of attribute rules. We also provide an appropriate error message if the message is not included in the attribute declaration.
The Validate
method of the base object EntityObject
is as follows
public class EntityObject : IEntityObject
{
#region Internal Fields
public List<error> Errors = new List<error>();
#endregion
.....
public virtual void Validate(object sender, ValidateEventArgs e)
{
Errors.Clear();
if (this.OnValidate != null) this.OnValidate(this, new ValidateEventArgs());
try
{
foreach (PropertyInfo info in this.GetType().GetProperties())
{
object data = info.GetValue(this, null);
foreach (object customAttribute in
info.GetCustomAttributes(typeof(DefaultValueAttribute), true))
{
if (data == null)
{
info.SetValue(this, (customAttribute
as DefaultValueAttribute).Default, null);
data = info.GetValue(this, null);
}
}
foreach (object customAttribute in
info.GetCustomAttributes(typeof(RequiredAttribute), true))
{
if (string.IsNullOrEmpty((string)data))
{
Errors.Add(new Error(this, info.Name,
string.IsNullOrEmpty((customAttribute
as RequiredAttribute).ErrorMessage) ?
string.Format("{0} is required", info.Name) :
(customAttribute as RequiredAttribute).ErrorMessage));
}
}
foreach (object customAttribute in
info.GetCustomAttributes(typeof(InRangeAttribute), true))
{
if (!(((IComparable)data).CompareTo((customAttribute as
InRangeAttribute).Min) > 0) ||
!(((IComparable)data).CompareTo((customAttribute as
InRangeAttribute).Max) < 0))
{
Errors.Add(new Error(this, info.Name,
string.IsNullOrEmpty((customAttribute
as InRangeAttribute).ErrorMessage) ?
string.Format("{0} is out of range", info.Name) :
(customAttribute as InRangeAttribute).ErrorMessage));
}
}
}
}
catch (Exception ex)
{
throw new Exception("Could not validate Object!", ex);
}
finally
{
if (this.OnValidated != null) this.OnValidated(this, new ValidateEventArgs());
}
}
}
7. Overriding the Validate Method
Shown below is code which shows how you would override the Validate
method of the business object and add custom validation rules:
public class Person : EntityObject
....
public override void Validate(object sender, ValidateEventArgs e)
{
base.Validate(sender, e);
if (this.Age == 25)
{ Errors.Add(new Error(this, "Person",
"A person cannot be 25 years of age!")); }
}
}
8. Other Considerations
- You may decide to have a
Validate
method in each attribute that takes the arguments from the reflected data and transforms it into an appropriate manner. This would enable you not to worry about testing for validity of the property value on the business objectn but let the business object call the instance of the attribute and validate values in an identical manner.
- When you need to data bind the business object on to your WinForms / WPF / WebForms, you may have to take advantage of the nice interfaces that are provided in the .NET framework to enable ErrorProviders to inform users on invalid entries.
IErrorInfo
INotifyPropertyChanged
- You may also decide to cache the properties and their attributes on business object creation to enable better performance. This would be the best approach if you wish to do immediate validation, rather than wait until the user saves the object.
- You may decide to have more complex attributes which call delegates on validation. This would be another way that would enable you to create more robust and flexible business rules.
- Using AOP (Aspect Oriented Programming) frameworks like PostSharp, you can add attributes to your object and perform validation, tracing, and other interesting things.
Please check Validation Aspects and the Validation Framework.
9. Conclusion
I hope this will help those of you who would like to understand what attribute based programming is. While writing this article, I realised that type-safety is something, as a programmer, you need to look at closely. Ensure that, for example, you use [DefaultValue(30)]
rather than a string [DefaultValue("30")]
.
10. History
- 12/22/2008: First posted.