Code Voyeur
RSS
Languages MVC ORM About Roadmap Contact Site Map RSS Sample Code Presentations Snippets dll Hell .net

A Simple IronPython Business Object Validation Framework

Business object validation may be driven by sources as disparate as XML and custom attributes. Small expression languages or XML flavors may be defined to augment validation behavior. This article will attempt to demonstrate the power and flexibility of IronPython by using it as a language for validating POCOs (plain old CLR objects). The validation framework presented here has not been tested for performance. Again, the focus is on extensibility with IronPython.

The validation framework will be modeled loosely after Castle's. A base class will exist, with a standard IsValid method.

public abstract class ValidationBase
{
    public ValidationRunnerBase Runner { get; set; }
    public Dictionary<string, string> PropertiesWithErrors { get; set; }

    public bool IsValid()
    {
                ...
    }
}
Classes that wish to use this framework will extend this base class.
public class User : ValidationBase { ... }
Properties from ValidationBase subclasses that require validation will be decorated with custom Validation attributes. The code for the custom attribute is rather common and will not be discussed here. Please see the sample project for details.
[Validation("first_name_length")]        
public string FirstName { get; set; }

[Validation("last_name_length")]
public string LastName { get; set; }

[Validation("password_contains_digits")]
public string Password { get; set; }
The actual execution of the validation rules will be delegated to a separate class, called a ValidationRunner. ValidationRunnerBase defines some basic functionality and required implements.
public abstract class ValidationRunnerBase
{
    public abstract ValidationResult Run(string ruleName, object propertyValue);

    protected bool Eval(string source, object propertyValue)
    {
                ...
    }
}
ValidationResult is simply a POCO defining properties to hold values for validation results and error messages. Again, there's nothing controversial here. Refer to the sample project for details.

Validating an instance of a ValidationBase subclass isn't particularly difficult. Essentially it amounts to checking the boolean IsValid method and optionally iterating over a list of error messages.

User user = new User();

Console.Write("Please enter a first name: ");
user.FirstName = Console.ReadLine();
...
 user.Runner = new XmlValidationRunner("ValidationRules.xml");

 if (!user.IsValid())
 {
     foreach (string propName in user.PropertiesWithErrors.Keys)
            {
                Console.WriteLine("\t{0} is invalid ({1}).", propName, user.PropertiesWithErrors[propName]);                        
            }
}
In the code above, the first view into how the actual validation rules are being executed occurs in the line that sets the Runner property of the User instance to a new instance of an XmlValidationRunner. This class is a subclass of ValidationRunnerBase.

XmlValidationRunner's implementation of ValidationRunner's abstract Run method, using LINQ to XML to query an XML document (path is passed in via the constructor). The expected format is shown below.

public override ValidationResult Run(string ruleName, object propertyValue)
{
    var rules =     from rule in _ruleDoc.Descendants("rule")
                            where rule.Attribute(XName.Get("name")).Value == ruleName
                            select new 
                            { 
                                Script = rule.Value.Trim() , 
                                Message = rule.Attribute(XName.Get("message")).Value 
                            } ;            

            foreach(var rule in rules)
            {
                if (! Eval(rule.Script, propertyValue))
                {
                    return new ValidationResult() { Value = false, ErrorMessage = rule.Message };
                }

                return new ValidationResult() { Value = true };
            }            
                
            throw new ArgumentException("No rule found for given name.");
}
<rules>
  <rule name="first_name_length" message="First name must be at least 3 characters">
    <![CDATA[result = property_value != None and len(property_value) >= 3]]>
  </rule>
</rules>
Looking at the XML above, it's clear that rules have names, messages and expressions. When IsValid is called, the ValidationBase instance's properties are interrogated for custom Validation attributes. For each found attribute, the name is passed to the Run method.
if (attr is ValidationAttribute)
{
    object value = prop.GetValue(this, null);
    ValidationResult result = Runner.Run(((ValidationAttribute)attr).RuleName, value);
    if (!result.Value)
        PropertiesWithErrors.Add(prop.Name, result.ErrorMessage);
}
Each run returning a ValidationResult with a false Value property will be added to the ValidationBase instance's PropertiesWithErrors dictionary. The message from the rule XML above is used (it's populated by the Runner) as the value to the dictionary.

Execution of the rule script is performed by a protected method within the ValidationRunnerBase class. In IronPython's library is a PythonEngine class, which allows for IronPython scripts to be executed within another .NET application. Simple methods allow strings containing IronPython scripts to be executed and evaluated.

Creating an instance of a PythonEngine is a relatively expensive operation. The framework caches an instance per type (admittedly not thread safe) to minimize the penalty.

private PythonEngine _engine;
private static Dictionary<string, PythonEngine> _engineCache = new Dictionary<string,PythonEngine>
The eval method is pretty simple, but accomplishes a great deal.
protected bool Eval(string source, object propertyValue)
{
    string typeName = this.GetType().Name;

    if (_engineCache.ContainsKey(typeName))
        _engine = _engineCache[this.GetType().Name] ;
    else
        _engine = _engineCache[typeName] = new PythonEngine();

    _engine.Execute("from System import *");

    _engine.Globals["property_value"] = propertyValue;
    _engine.Globals["result"] = true;
    _engine.Execute(source);
    
    bool result = (bool)_engine.Globals["result"];

    _engine.Shutdown();
    return result;
}
First, the type name is used to pull an engine instance out of cache (or cache and create if not yet created). After that, the engine executes its first line of code. Specifically, System is added to engine's list of imports (think C# using or VB.NET Import). This step allows the scripts in the XML to use System's standard type names (i.e., Int32.Parse(“1”)). Next, the two lines adding to the Globals dictionary create two new global variables accessible within the stored scripts. The next line actually executes the script.

The Eval method (and the framework in general) relies two assumptions about the scripts in the XML.

  1. The value of the property to be validated is available in the global variable “property_value.”
  2. The result of the property value validation is set to the global variable “result.”
Any script that adheres to the contract of the above assumptions is valid. Consider the following:
<rule name="password_contains_digits" message="Password must contain at least one number">
def word_contains_digit(word):
  for char in word:    
    if Char.IsDigit(char):
      return True
  return False

result = word_contains_digit(property_value)
  </rule>
The script defines a function “word_contains_digit” that is called with “property_value” and has its return value assigned to the global “result.” The engine is then able to read the value of the global after executing the source.
bool result = (bool)_engine.Globals["result"];
This validation framework simply maps object properties to validation rules by way of custom attributes containing rule names. It's a simple scheme that's potentially rather powerful. The engine has functionality for adding assembly references, additional imports and globals. This extensibility allows for virtually any type of validation rule to be set with a single custom attribute. Moreover, the class model as it exists could easily be augmented to support other rule sources (database, web service).

Download Sample Project

References

A place to post comments on this article until I have time to add a comments feature to Code Voyeur...
Sample project source code
Article Posted: Tuesday, April 08, 2008

Leave a Comment


Contact Code Voyeur about this article.