Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

An attribute based approach to business object validation

0.00/5 (No votes)
22 Dec 2008 1  
An article showing how you can use attributes to validate your business objects.

Attribute_Validation_src

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].

/// <summary>
/// Person class.
/// This class represents the person 
/// Author: Malisa Ncube
/// </summary>
public class Person : EntityObject
{
    /// <summary>
    /// Property that describes the Title of this <see cref="Person">
    /// </see></summary>
    [DefaultValue("Mr")]
    public string Title { get; set; }

    /// <summary>
    /// Property that describes the FirstName of this <see cref="Person">
    /// </see></summary>
    [Required]
    public string FirstName { get; set; }

    /// <summary>
    /// Property that describes the LastName of this <see cref="Person">
    /// </see></summary>
    [Required(ErrorMessage = "LastName must have a value")]
    public string LastName { get; set; }

    /// <summary>
    /// Property that describes the Age of this <see cref="Person">
    /// </see></summary>
    [InRange(18, 95)]
    [DefaultValue(30)]
    public int? Age { get; set; }

    public override void Validate(object sender, ValidateEventArgs e)
    {
        base.Validate(sender, e);

        //Custom business rules
        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.

// Create new instance of the Person class
Person person = new Person();

//Assign values to properties

person.Firstname = "John";
person.Lastname = "Doe";

person.Age = 15;
//Should cause the object to be invalid and fail to save

string errMsg = "Could not save Person!\n\n"; 

if (!person.Save())
{
    //Collect all error messages into one error string
    foreach(Error err in person.Errors) 
    {
        errMsg += err.Message + "\n";
    }
    MessageBox.Show(errMsg, "Error", MessageBoxButtons.OK, 
                    MessageBoxIcon.Exclamation);
}
else
{
    //We show the following message if the person object is valid
    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.

/// <summary>
/// EntityObject class.
/// This Entity base class 
/// Author: Malisa Ncube
/// </summary>
public class EntityObject : IEntityObject
{
    
    #region Internal Fields
    /// <summary>
    /// The Errors collection to keep the errors. Tthe validation method populates this.
    /// </summary>
    public List<error> Errors = new List<error>();

    #endregion

    #region Delegate and Events
    /// <summary>
    /// OnValidateEventHandler delegate to enable injection of custom validation routines
    /// </summary>
    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
    /// <summary>
    /// The Errors collection to keep the errors. Tthe validation method populates this.
    /// </summary>
    public List<error> Errors = new List<error>();

    #endregion

   .....

    /// <summary>
    /// Validate method performs the validation process and allows overriding 
    /// </summary>
    public virtual void Validate(object sender, ValidateEventArgs e)
    {
        //Initialise the error collection
        Errors.Clear();

        //Enable calling the OnValidate event before validation takes place
        if (this.OnValidate != null) this.OnValidate(this, new ValidateEventArgs());
        try
        {

            foreach (PropertyInfo info in this.GetType().GetProperties())
            {
                /* Get property value assigned to property */
                object data = info.GetValue(this, null);

                /* Set Default value if value is empty */
                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);
                    }
                }

                /* Check if property value is required */
                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));
                    }
                }

                /* Evaluate whether the property value lies within range */
                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
        {
            //Enable calling the OnValidated event after validation has taken place
            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);

        //Custom business rules
        if (this.Age == 25) 
        { Errors.Add(new Error(this, "Person", 
                     "A person cannot be 25 years of age!")); }
    }
}

8. Other Considerations

  1. 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.
  2. 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
  3. 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.
  4. 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.
  5. 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.

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