Monday, March 9, 2009

An Asp.Net Validation Framework (Part 2)

In the first part, we saw how to create custom attributes and rules. Now we'll look at how to implement those attributes.

The IValidatable interface defines how consumers can interact with the framework. It indicates that we'll have a collection of BrokenRules and offers a method to perform validation.

ValidatableBase implements IValidatable and does most of the work. Your classes simply inherit from ValidatableBase and they are completely functional. You decorate all of the properties you want.

/// <summary>
/// Interface for class that wish to implement
/// validation.
/// </summary>
public interface IValidatable
{
    /// <summary>
    /// Gets the collection of business rules that have 
    /// been broken.
    /// </summary>
    BrokenRulesCollection BrokenRules { get; }

    /// <summary>
    /// Validates the objects.
    /// </summary>
    /// <param name="clearBrokenRules">Indicates if the BrokenRules collection
    /// should be cleared of prior to validation.</param>
    /// <returns>True if the object is valid, otherwise false.</returns>
    bool Validate(bool clearBrokenRules);
}

/// <summary>
/// Base class for implementation of validation
/// through attributes.
/// </summary>
public abstract class ValidatableBase
    : IValidatable
{
    /// <summary>
    /// Holds the collection of broken business rules.
    /// </summary>
    private BrokenRulesCollection brokenRules;

    /// <summary>
    /// Initializes a new instance of the ValidatableBase class.
    /// </summary>
    public ValidatableBase()
    {
        this.brokenRules = new BrokenRulesCollection();
    }

    /// <summary>
    /// Gets the collection of business rules that have 
    /// been broken.
    /// </summary>
    public BrokenRulesCollection BrokenRules
    {
        get
        {
            return this.brokenRules;
        }
    }

    /// <summary>
    /// Validates the objects.
    /// </summary>
    /// <returns>True if the object is valid, otherwise false.</returns>
    public virtual bool Validate()
    {
        return this.Validate(true);
    }

    /// <summary>
    /// Validates the objects.
    /// </summary>
    /// <param name="clearBrokenRules">Indicates if the BrokenRules collection
    /// should be cleared of prior to validation.</param>
    /// <returns>True if the object is valid, otherwise false.</returns>
    public virtual bool Validate(bool clearBrokenRules)
    {
        if (clearBrokenRules)
        {
            this.BrokenRules.Clear();
        }

        PropertyInfo[] properties = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);

        var valProps = from PropertyInfo property in properties
                       where property.GetCustomAttributes(typeof(ValidationAttribute), true).Length > 0
                       select new
                       {
                           Property = property,
                           ValidationAttributes = property.GetCustomAttributes(typeof(ValidationAttribute), true)
                       };

        foreach (var item in valProps)
        {
            foreach (ValidationAttribute attribute in item.ValidationAttributes)
            {
                if (attribute.IsValid(item.Property.GetValue(this, null), item.Property.Name))
                {
                    continue;
                }

                this.BrokenRules.Add(attribute.Rule);
            }
        }

        return this.BrokenRules.Count == 0;
    }
}

So what does it do? Basically, the Validate method iterates through all properties that have at least one ValidationAttribute applied. It then iterates through each property's ValidationAttributes and checks IsValid. If it is false, it adds the Rule from the attribute to the BrokenRules collection. This is all done using reflection.

Here's the my version of BrokenRulesCollection. It uses Collection<> but you could just as easily use a List<>.

/// <summary>
/// Class to hold BrokenRules.
/// </summary>
[Serializable]
public class BrokenRulesCollection
    : Collection<BrokenRule>
{
    /// <summary>
    /// Adds a new BrokenRule to the collection with
    /// the specified property name and error message.
    /// </summary>
    /// <param name="propName">The name of the property that was invalid.</param>
    /// <param name="msg">The error message.</param>
    public void Add(string propName, string msg)
    {
        this.Add(new BrokenRule(propName, msg));
    }
}

Please look at Imar's posts as they really clarified the idea for me.

Next, we’ll look at how to implement this strategy with Linq To Sql and the Entity Framework.

No comments: