Click here to Skip to main content
15,879,613 members
Articles / Programming Languages / C#
Article

Validation Across Class Hierarchies and Interface Implementations

Rate me:
Please Sign up or sign in to vote.
4.00/5 (4 votes)
12 May 2008LGPL33 min read 28.8K   100   25   2
Dependency injection of validation rules and their application across class hierarchies and interface implementations

Introduction

Correct and comprehensive validation of the state of business objects is a critical requirement in the development of every data driven application. This article will demonstrate how to instrument POCOs (plain old C# objects) with support for validation rules using dependency injection at runtime.

Implementation Objectives

  • Centralize the definition of validation rules into one library in order to eliminate duplication of code that would otherwise become a maintenance headache and be potentially inconsistent.
  • Provide the ability to define the application of validation rules to objects at runtime in order to maximize the flexibility of the application by not hardcoding validation rules at compile time.
  • Allow the redefinition of what validation rules are to be applied to objects at run time using a configuration file and have that redefinition picked up and applied to objects without having to recompile code or even stop/restart the application.
  • Provide the ability to define the application of validation rules to both Interfaces and Classes and have those rules applied across Class hierarchies and Interface implementations.

Motivating Example

I will not attempt to present a full-blown LOB application, rather I will provide a simple example that demonstrates the applicability of this approach.

Let us suppose that we wish to model a person using an IPerson interface. The IPerson interface will expose the following properties.

  • First name
  • Last name
  • Date of birth

The specification of the IPerson interface will be as follows:

C#
public interface IPerson {

        string FirstName {
            get;
            set;
        }

        string LastName {
            get;
            set;
        }

        DateTime DateOfBirth {
            get;
            set;
        }
    }

We shall then create an abstract class Person that provides a default implementation of the IPerson interface.

The specification of the Person abstract class will be as follows:

C#
public abstract class Person : IPerson {

        private string firstName;
        private string lastName;
        private DateTime dateOfBirth;

        public virtual string FirstName {
            get {
                return firstName;
            }
            set {
                firstName = value;
            }
        }

        public virtual string LastName {
            get {
                return lastName;
            }
            set {
                lastName = value;
            }
        }

        public virtual DateTime DateOfBirth {
            get {
                return dateOfBirth;
            }
            set {
                dateOfBirth= value;
            }
        }
    }

Finally, we shall define two concrete classes Employee and Customer that inherit from Person.

The specification of the Employee and Customer classes will be as follows:

C#
public class Employee : Person {
    }
public class Customer : Person {
}

In a real-world application, the Employee and Customer classes would further specialize the Person class, but for the sake of simplicity, we shall not do so here.

The Validation Rules

We shall require any class that implements the IPerson interface to conform to the following rules:

  • If the first name is specified, it must consist of only letters.
  • If the first name is specified, it must be between 2 and 15 letters.
  • The last name must be specified, i.e. cannot be null.
  • The last name must consist of only letters.
  • The last name must be between 2 and 15 letters.
  • The age must be between 1 and 120 (calculated using date of birth).

For instances of the Employee class, we shall further require that:

  • The first name must be specified.
  • The age must be between 18 and 65.

Implementation of Validation Rules

The validation rules will be coded up in the class Validator. The implementation will be as follows:

C#
public class Validator {

    // Returns KeyValuePair<true, IBrokenRule> 
    // if value has any non-letter characters
    public KeyValuePair<bool, IBrokenRule> ValidateIsAlpha
        (object instance, object value, object[] arguments) {

        var brokenRule = new BrokenRule
        ("ValidateIsAlpha", "Only letters are allowed");
    
        var stringValue = value as string;
        
        if (stringValue != null && stringValue.Any(c => !char.IsLetter(c))) {
            return new KeyValuePair<bool,IBrokenRule>(true, brokenRule);
        }

        return new KeyValuePair<bool, IBrokenRule>(false, brokenRule);
    }

    // Returns KeyValuePair<true, IBrokenRule> 
    // if value is null or value == string.Empty
    public KeyValuePair<bool, IBrokenRule> 
        ValidateIsNotBlank(object instance, object value, object[] arguments) {

        var stringValue = (value as string);
        
        var brokenRule = new BrokenRule("ValidateIsNotBlank", "Value cannot be blank");
    
        if (stringValue == null || stringValue == string.Empty) {
            return new KeyValuePair<bool, IBrokenRule>(true, brokenRule);
        }

        return new KeyValuePair<bool, IBrokenRule>(false, brokenRule);
    }

    // Returns KeyValuePair<true, IBrokenRule> if value is not 
    // an integral number or value
    // is not between the minimum and maximum values specified in 
    // arguments[0] and arguments[1]
    public KeyValuePair<bool, IBrokenRule> ValidateIntegralNumberIsBetween
        (object instance, object value, object[] arguments) {
        double doubleValue;
        double minValue;
        double maxValue;

        GetMinMaxValue(arguments, out minValue, out maxValue);
        
        var brokenRule = new BrokenRule("ValidateIntegralNumberIsBetween",
        string.Format("Value must be between {0} and {1}", 
            minValue, maxValue));

        if (!double.TryParse((value.ToString()).Trim(), out doubleValue) ||
            doubleValue < minValue || doubleValue > maxValue) {
            return new KeyValuePair<bool, IBrokenRule>(true, brokenRule);
        }

        return new KeyValuePair<bool, IBrokenRule>(false, brokenRule);
    }

    // Utility function to extract the minimum and maximum value from 
    // arguments[0] and arguments[1]
    private static void GetMinMaxValue(object[] arguments, 
        out double minValue, out double maxValue) {
            
        Debug.Assert(arguments != null && arguments.Length == 2);

        Debug.Assert((arguments[0] as string) != null);

        Debug.Assert((arguments[1] as string) != null);

        var validMinValue = double.TryParse((arguments[0] as string).Trim(), 
            out minValue);

        var validMaxValue = double.TryParse((arguments[1] as string).Trim(), 
            out maxValue);

        Debug.Assert(validMinValue && validMaxValue);
    }

    // Returns KeyValuePair<true, IBrokenRule> if the length of value 
    // is not an integral number
    // between the minimum and maximum values specified in 
    // arguments[0] and arguments[1]
    public KeyValuePair<bool, IBrokenRule> ValidateStringLengthIsBetween
        (object instance, object value, object[] arguments) {

        double minValue;
        double maxValue;
    
        GetMinMaxValue(arguments, out minValue, out maxValue);

        var brokenRule = new BrokenRule("ValidateStringLengthIsBetween", 
            string.Format("Value must have between 
            {0} and {1} characters", 
            minValue, maxValue));

        var stringValue = value as string;

        if (stringValue != null && stringValue != string.Empty &&
            (stringValue.Length < minValue || stringValue.Length > maxValue)) {
                return new KeyValuePair<bool, IBrokenRule>(true, brokenRule);
        }

        return new KeyValuePair<bool, IBrokenRule>(false, brokenRule);
    }

    // Returns KeyValuePair<true, IBrokenRule> if the age 
    // (calculated using value) is not an integral number
    // between the minimum and maximum values specified in 
    // arguments[0] and arguments[1]
    public KeyValuePair<bool, IBrokenRule> ValidateAgeIsBetween
        (object instance, object value, object[] arguments) {

        double minValue;
        double maxValue;
    
        GetMinMaxValue(arguments, out minValue, out maxValue);
    
        var brokenRule = new BrokenRule("ValidateAgeIsBetween",
            string.Format("Age must be between {0} and {1}",
            minValue, maxValue));

        if (value == null) {
            return new KeyValuePair<bool, IBrokenRule>(false, brokenRule);
        }

        DateTime dateTime;
 
        var isValid = DateTime.TryParse(value.ToString(), out dateTime);
    
        if (!isValid || 
            ValidateIntegralNumberIsBetween(instance, DateTime.Now.Year - 
            dateTime.Year, arguments).Key) {
                return new KeyValuePair<bool, IBrokenRule>(true, brokenRule);
        }

        return new KeyValuePair<bool, IBrokenRule>(false, brokenRule);
    }
}

Definition of the Application of Validation Rules

The definition of the application of validation rules will be specified in an XML file rules.xml. That will look as follows:

XML
<Rules>
  <Class name="IPersonAndImplementations.IPerson, IPersonAndImplementations, 
        Version=1.0.0.0" >
    <Property name="FirstName">
      <Rule assemblyName="SampleRules, Version=1.0.0.0, 
              Culture=neutral, PublicKeyToken=null" 
            class="SampleRules.Validator" method="
            ValidateIsAlpha" arguments=""/>
      <Rule assemblyName="SampleRules, Version=1.0.0.0, 
              Culture=neutral, PublicKeyToken=null" 
            class="SampleRules.Validator" method="
            ValidateStringLengthIsBetween" arguments="2,15"/>
    </Property>
    <Property name="LastName">
      <Rule assemblyName="SampleRules, Version=1.0.0.0, 
              Culture=neutral, PublicKeyToken=null"
            class="SampleRules.Validator" method="
            ValidateIsAlpha" arguments=""/>
      <Rule assemblyName="SampleRules, Version=1.0.0.0, 
              Culture=neutral, PublicKeyToken=null" 
            class="SampleRules.Validator" method="
            ValidateIsNotBlank" arguments=""/>
      <Rule assemblyName="SampleRules, Version=1.0.0.0, 
              Culture=neutral, PublicKeyToken=null" 
            class="SampleRules.Validator" method="
            ValidateStringLengthIsBetween" arguments="2,15"/>
    </Property>
    <Property name="DateOfBirth">
      <Rule assemblyName="SampleRules, Version=1.0.0.0, 
              Culture=neutral, PublicKeyToken=null" 
            class="SampleRules.Validator" method="
            ValidateAgeIsBetween" arguments="1, 120"/>
    </Property>
  </Class>
  <Class name="IPersonAndImplementations.Employee, IPersonAndImplementations, 
          Version=1.0.0.0, Culture=neutral" >
    <Property name="FirstName">
      <Rule assemblyName="SampleRules, Version=1.0.0.0, 
              Culture=neutral, PublicKeyToken=null"
            class="SampleRules.Validator" method="
            ValidateIsNotBlank" arguments=""/>
    </Property>
    <Property name="DateOfBirth">
      <Rule assemblyName="SampleRules, Version=1.0.0.0, 
              Culture=neutral, PublicKeyToken=null"
            class="SampleRules.Validator" method="
            ValidateAgeIsBetween" arguments="18, 65"/>
    </Property>
  </Class>
</Rules>

The schema for rule definition should be fairly self-explanatory, but will be covered in more detail in Part II of the series.

Putting It All Together

You will have noticed that all the properties defined in the Person class are declared as being virtual. We shall make use of the LinFu Framework to create proxies of the Employee and Customer instances at run time. Calls to the property accessors will then be intercepted and validation rules applied accordingly.

I have created a ProxyManager class to manage this process. In addition, the proxies will automatically implement the following interfaces:

  • IBrokenRuleConsumer
  • IDataErrorInfo
  • INotifyPropertyChanged
  • INotifyPropertyChanging

I will provide further information on the implementation of the ProxyManager and details of the IBrokenRuleConsumer interface in Part II of this series.

Seeing It In Action

Image 1

The relevant lines of code to create the proxies and implement data binding are listed below:

C#
public frmEmployee() {

    InitializeComponent();
    var list = new LinkedList<object>();
    // Create a proxy that is an instance of Person that has a  
    // dynamic implementation of IBrokenRuleConsumer, 
    // IDataErrorInfo, INotifyPropertyChanged, INotifyPropertyChanging 
    list.AddFirst(ProxyManager.Create<Employee>());
    errorProvider = new ErrorProvider(this);
    this.components = new System.ComponentModel.Container();
    personBindingSource = new BindingSource(this.components);
    ((System.ComponentModel.ISupportInitialize)(this.personBindingSource)).BeginInit();
    personBindingSource.DataSource = list;
    txtFirstName.DataBindings.Add(new Binding
        ("Text", this.personBindingSource, "FirstName", true));
    txtLastName.DataBindings.Add(new Binding
        ("Text", this.personBindingSource, "LastName", true));
    mtbDateOfBirth.DataBindings.Add(new Binding
        ("Text", this.personBindingSource, "DateOfBirth", true));
    (list.First.Value as IBrokenRuleConsumer).EnforceConstraints();
    errorProvider.BlinkStyle = ErrorBlinkStyle.NeverBlink;
    errorProvider.DataSource = personBindingSource;
    ((System.ComponentModel.ISupportInitialize)(this.personBindingSource)).EndInit();
}

They say a picture is worth a thousand words. Downloading and experimenting with the samples should be worth a million more. I hope you will enjoy.

Cheers!

History

  • 12th May, 2008: Initial post

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Technical Lead Olivine Technology
Kenya Kenya
Technical Lead, Olivine Technology - Nairobi, Kenya.

"The bane of productivity: confusing the rituals of work (sitting at your desk by 8:00am, wearing a clean and well pressed business costume etc.) with actual work that produces results."

Watch me!

Comments and Discussions

 
Questionhow to do without LinFu framework Pin
Thanks for all the fish12-May-08 6:02
Thanks for all the fish12-May-08 6:02 
AnswerRe: how to do without LinFu framework [modified] Pin
Muigai Mwaura13-May-08 4:33
Muigai Mwaura13-May-08 4:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.