Code Voyeur
RSS
Data Access Languages MVC ORM About Roadmap Contact Site Map RSS Sample Code Presentations Snippets dll Hell .net Rate My Snippet

A Boo Object Validation DSL

Validation frameworks typically come in two flavors. The first ties validation to object state through attributes set on properties. This is the flavor favored by Castle's validation framework and a previous Code Voyeur article on business object validation (A Simple IronPython Business Object Validation Framework). The second flavor disconnects an object and its validator. Validation rules are typically defined in external configuration and executed through methods not found on the validated object. This approach is favored by Spring.NET and will also be used in validation framework for this article.

The goal of this framework is to allow for general purpose validation. This framework will not require inheritance and will not use attributes. This approach will provide the ability to validate business objects, Windows and Web forms with the same framework. The sample project for this article demonstrates how to use this framework to validate a web form (ASP.NET MVC) and business object.

The DSL will be very simple, defining only six keywords - "rule_for", "validate", "pass", "fail", "warn" and "create_results." The rule_for function will define the rule name and rule method. The validate function provides the actual validation rules. The pass, fail, warn and create_result keywords are helpers for creating validation results.

rule_for "LoginForm":
 
  validate def(x):
         
      results = create_results()    
        ...
          
      return results
The ValidationResult class is a POCO containing the result of a validation routine and any associated message. This class does not declare all results failures, allowing for the granular setting of validation statuses.
public enum ResultType { Pass, Fail, Warn }

public class ValidationResult {

    public ResultType ResultType { get; set; }

    public string Message { get; set; }

    public ValidationResult(string message, ResultType resultType) {
        Message = message;
        ResultType = resultType;
    }
}
A single class does most of the work for the framework - ValidationContext. ValidationContext will read (hard-coded path for the sample) a boo file and parse the defined rules.
public class ValidationContext {
    ...
}
ValidationContext has a static, overloaded method "Initialize." This method is expected to be called once per application and requires the path to the validation rules file. An overload allows for forcing a reinitialization of the rules with each validation attempt.
public static void Initialize(string filePath) {
    Initialize(filePath, false);
}

public static void Initialize(string filePath, bool reInitOnValidate) {

    _reInitOnValidate = reInitOnValidate;
    _filePath = filePath;           
    new ValidationContext().init();
    _initialized = true;
}
The private init method called above begins by newing up a Dictionary to hold rules as well as the Boo InteractiveInterpreter. The interpreter is set to Ducky, which allows Boo to act like a dynamic language (objects without explicit types are not checked for valid method and property definitions until runtime).
_rules = new Dictionary<string, Dictionary<string, Func<object, List<ValidationResult>>>>();
_interpreter = new InteractiveInterpreter();
_interpreter.Ducky = true;
The next line calls addValidationFunctions() which is responsible for createing the rule_for and validate functions.
private void addValidationFunctions() {
           
    Action<string, Action> ruleFor = delegate(string name, Action action) {

        if (!_rules.ContainsKey(name)) {
            _rules[name] = new Dictionary<string, Func<object, List<ValidationResult>>>();
            _ruleName = name;
            action(); //this calls validate
        }
    };

    Action<Func<object, List<ValidationResult>>> validate = delegate(Func<object, List<ValidationResult>> func) {
        _rules[_ruleName]["validate"] = func;
    };

    _interpreter.SetValue("rule_for", ruleFor);
    _interpreter.SetValue("validate", validate);

}
The ruleFor delegate has two parameters, the name of the rule and an action. The name is used by ValidationContext to find the associated rule when validation is performed. The action is called in the body of the delegate, which when executed by the Boo interpreter puts the name of the rule in the the _rules Dictionary. It also creates a place holder for a Validation function (the actual rule is stored in the new Dictionary).

The next chunk of code defines the second keyword, "validate." The validate delegate takes a Func as its input, which takes as input an instance of T and returns a list of type ValidationResult.

When validate is executed by the Boo interpreter, the func argument is placed in the Dictionary that was newed up in ruleFor.

Next, these two delegates are passed to the Boo script as global values.

_interpreter.SetValue("rule_for", ruleFor);
_interpreter.SetValue("validate", validate);
Next, the pass, fail, warn and create_results delegates are passed to the script when addPassWarnFailFunctions is called. These functions are simply shortcut functions for creating ValidationResult instances.
private void addPassWarnFailFunctions() {

    Func<List<ValidationResult>> createResults = (() => new List<ValidationResult>());
    Func<string, ValidationResult> pass = (m => new ValidationResult(m, ResultType.Fail));
    Func<string, ValidationResult> fail = (m => new ValidationResult(m, ResultType.Fail));
    Func<string, ValidationResult> warn = (m => new ValidationResult(m, ResultType.Warn));

    _interpreter.SetValue("create_results", createResults);
    _interpreter.SetValue("pass", pass);
    _interpreter.SetValue("fail", fail);
    _interpreter.SetValue("warn", warn);
}
Finally, the code is compiled and errors checked.
CompilerContext ctx = _interpreter.EvalCompilerInput(new FileInput(filePath));

if (ctx.Errors.Count != 0) {
    StringBuilder sb = new StringBuilder();
    foreach (CompilerError error in ctx.Errors) {
        sb.AppendLine(error.Message);
    }

    throw new ApplicationException(sb.ToString());
}
This code might be a little confusing, given the switching in and out of the interpreter's context. But essentially what happens is when the interpreter evaluates the .boo file, a series of rule_for calls are made. Each time the interpreter evaluates rule_for, the ruleFor delegate code is executed. This delegate creates an entry in _rules (Dictionary) using the name passed in (from the boo file). The new _rules entry has a place holder for a delegate. Then the action argument is invoked. Invoking ruleFor's action argument (which in the boo file is a call to validation) executes the validation delegate. The call to validate is made with another delegate as its argument. The validate delegate takes its func argument and stores it in the _rules dictionary. In short, when the interpreter calls rule_for, validate is called. When validate is called, the block argument is stored in the _rules dictionary for later execution.

Actual object validation is performed when the Validate method is called. This method is overloaded to allow for a default rule-by-type to be defined.

public void Validate<T>(T o) {
    Validate(o, o.GetType().Name);
}

public void Validate<T>(T o, string name) {

    if (!_initialized)
        throw new ApplicationException("Validation rules have not been initialized.  Call Initialize before attempting validation.");

    if (_reInitOnValidate) init();
    if (_rules.ContainsKey(name)) {
        _validationResults = _rules[name]["validate"].Invoke(o);
    } else
        throw new ApplicationException("Invalid rule name");
}
When Validate is called, the rule is executed and results are returned to _validationResults (exposed via the ValidationResults property). If Initialize was called with reInitOnValidate set to true, init is called with each validation request. This setting is useful mostly for debugging the validation script during development.

For clients easily to check for errors or warnings, HasErrors and HasWarnings properties are included. Similarly, properties for ErrorMessages and WarningMessages are also included.

public bool HasErrors {

    get {
        return _validationResults.Where(r => r.ResultType == ResultType.Fail).Count() > 0;
    }
}

public IList<string> ErrorMessages {
    get {
        return _validationResults
            .Where(r => r.ResultType == ResultType.Fail)
            .Select(a => a.Message).
            ToList();
    }
}
The sample application for this article is an ASP.NET MVC project. In the project, there is a simple login form that is validated using both direct form validation and business object validation. The validation rules are initialized in Application_Start in Global.asax.
protected void Application_Start() {
    RegisterRoutes(RouteTable.Routes);

    ValidationContext.Initialize(HttpContext.Current.Server.MapPath("~/App_Data/Validation.boo"), true);
}
In one controller action, the form collection is passed to the Validate method.
public ActionResult ValidateForm() {

    ValidationContext ctx = new ValidationContext();           
    ctx.Validate<NameValueCollection>(Request.Form, "LoginForm");

    if (ctx.HasErrors) {
        TempData["Errors"] = ctx.ErrorMessages;
    } else {
        TempData["Message"] = "You have successfully passed validation";
    }

    return RedirectToAction("Index");
}
The validation routine in Validation.boo is written to work against a NameValueCollection.
public ActionResult ValidateBusinessObj(string username, string password) {

    User user = new User() { Username = username, Password = password };

    ValidationContext ctx = new ValidationContext();           
    ctx.Validate<User>(user);

    if (ctx.HasErrors) {
        TempData["Errors"] = ctx.ErrorMessages;
    } else {
        TempData["Message"] = "You have successfully passed validation";
    }

    return RedirectToAction("Index");
}
The other controller action initializes a User instance using the form values.
public ActionResult ValidateBusinessObj(string username, string password) {

    User user = new User() { Username = username, Password = password };

    ValidationContext<User> ctx = new ValidationContext<User>();
    ctx.Reset();
    ctx.Validate(user);

    if (ctx.HasErrors) {
        TempData["Errors"] = ctx.ErrorMessages;
    } else {
        TempData["Message"] = "You have successfully passed validation";
    }

    return RedirectToAction("Index");
}
The validation routine is then written against an instance of User.
rule_for "User":
 
  validate def(x):
         
      results = create_results()
     
      if string.IsNullOrEmpty(x.Username) or string.IsNullOrEmpty(x.Password):
        results.Add(fail("Username and Password are required"))
        return results
       
      if x.Username.Contains("fourletterword") or x.Password.Contains("fourletterword"):
        results.Add(fail("Username and password cannot contain 4 letter words"))
                      
      return results
This validation DSL is clearly simplistic, but is arguably sufficient for its purpose. That purpose is to provide a reusable and flexible object validation framework. A more complete validation DSL might include keywords for common validation routines (is null, email, etc.). However, given that the validation configuration is a Boo script, common routines could easily be imported via a common validation assembly.

Download Sample Project

Article Posted: Sunday, November 09, 2008

Leave a Comment

Shout it

Contact Code Voyeur about this article.