Geeks With Blogs
Malisa Ncube - .NET Delights .NET Development ideas and things


Attribute based validation for business objects

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 Ennis Ray Lynch, Jr.

2. Background

The .NET compiler enables us to richly embed metadata into an assembly, which can be accessed at a later time by reflection. Attributes are decorative clauses that can be placed on either 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 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/p>

[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 is 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.

/// <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!")); }

    }

}

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]

 

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 have 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 a enum in which you can choose the scope of where this attribute can be used. The default is All, however you can have Assembly, Class, Constructor, Delegate, Enum, Event, Field, Interface, method, Parameter, Property, ReturnValue and struct.

You can use 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 only 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 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 to 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./p>

    ///     

    /// EntityObject class.    

    /// This Entity base class     

    /// Author: Malisa Ncube    

    ///     public class EntityObject : IEntityObject    

    {                 

        #region Internal Fields        

    ///         

    /// The Errors collection to keep the errors. Tthe validation method populates this.        

        public List Errors = new List();

        #endregion

 

        #region Delegate and Events

        ///         

        // OnValidateEventHandler delegate to enable injection of custom validation routines        

        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 first will then use PropertyInfo to keep all the properties of the object and 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 was not included in the attribute declaration.

The validate method of the base object EntityObject is as follows

 

        #region Internal Fields

        ///         

        /// The Errors collection to keep the errors. Tthe validation method populates this.        

        public List Errors = new List();

 

        #endregion

 

       .....

 

        ///         

        /// Validate method performs the validation process and allows overriding         

        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

Below is some code which shows how you would override the Validate method of the business object and add custom validation rules.

   ....

 

 

        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

a. You may decide to have the a Validate method in each attribute that takes the arguments from the reflected data and transform it into a appropriate manner. This would enable you not to worry about testing for validity of the property value on the business object but let the business object call the instance of the attribute and validate values in an identical manner.

b. When you need to databind the business object on to you winforms / WPF / WebForms you may have to take advantage of the nice interfaces that are provided in the .NET framework to enable ErrorProviders inform users on invalid entries.

IErrorInfo
INotifyPropertyChanged

c. You may also decide to cache the properties and their attributes on business object creation to enable better performance. This would be a best approach if you wish to do immediate validation, rather than wait until the user saves the object.

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

e. Using AOP (Aspect oriented programming) frameworks like PostSharp you can add attributes to you 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 there is 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.

 

Source code for this article is available on CodeProject http://www.codeproject.com/KB/cs/AttributeBaseValidation.aspx

 

Technorati Tags: ,,
Posted on Monday, December 22, 2008 12:51 AM | Back to top


Comments on this post: An attribute based approach to business object validation

No comments posted yet.
Your comment:
 (will show your gravatar)


Copyright © Malisa L. Ncube | Powered by: GeeksWithBlogs.net