Introduction
There is a basic need for validation of objects at all levels. ASP.NET provides this service at the presentation layer, but there is really no formal way to achieve this at the business layer.
Problem
I've been playing around a lot with O/R mappers and have come to realize that it sucks with hand-coding validation all the time. It is quite repetitive and boiler plate code that simply gets boring. Castle's Active Record(which is quite good) has a validation system built in. It is based on Ruby(on Rails)'s ActiveRecord pattern which has an even more robust validation system. The problem I have found is that the validation framework is tightly coupled with the underlying entity model. There is no way to leverage the framework elsewhere.
Initial Shoutouts
I've been working for a little while on this and recently found another Validation Framework and read through the source code. It had some good ideas which I selectively incorporated into this framework (the CompareValidator
is the main one). He mentions an article he read which, by circumstance, I also read and got some ideas.
ValidationManager
There is really just one main class, the ValidationManager
. This class represents the central repository for validators. It has 2 static
methods: one to add a Validator
to a type and one to validate an instance. For example:
Validator val = RequiredValidator.CreateValidator<TestClass>("TestProperty");
ValidationManager.AddGlobalValidator<TestClass>(val);
TestClass tc = new TestClass();
if(ValidationManager.IsValid(tc))
MessageBox.Show("WOO HOO");
This code has now registered a validator with the type TestClass
for all new instances. In addition, any instances of TestClass
already created are notified of the addition and they refresh their validator cache. (This method of validation is not preferable because of the overhead involved in instantiating a ValidationManager
instance.)
Hmmm... what is the validator cache? The validator cache is an instance reference to the global validator cache for the instance's type. This means that we can also add instance validators as opposed to global validators. For example:
TestClass tc = new TestClass();
ValidationManager vm = new ValidationManager(tc);
Validator val = RequiredValidator.CreateValidator<TestClass>("TestProperty");
vm.AddValidator(val);
This code has now registered a validator with the tc
instance. No other instance of TestClass
has this required validator on it. You'll also notice that ValidationManager
takes an object as a constructor parameter. This instance of ValidationManager
is tied to the instance of the object passed in. For example:
TestClass tc = new TestClass();
ValidationManager vm = new ValidationManager(tc);
if(vm.IsValid())
MessageBox.Show("WOO HOO");
Attribute Based Validation
The framework, while able to be manipulated programatically, is much easier to use through attributes. For example:
public class TestClass
{
private string testProperty;
[RequiredValidator]
[LengthValidator(1,10)]
public string TestProperty
{
get { return this.testProperty; }
set { this.testProperty = value; }
}
}
...
TestClass tc = new TestClass();
ValidationManager vm = new ValidationManager(tc);
if(vm.IsValid())
MessageBox.Show("WOO HOO");
When a ValidationManager
is instantiated, it scans TestClass
via reflection for ValidatorAttribute
s and adds them to the global validator cache. It only does this once per type so the overhead happens only once.
Each validator attribute contains the common properties ErrorMessage
and Order
. These can be specified as named parameters but are not required. ErrorMessage
has a default and Order
indicates the order in which to process the validator if there are more than one. (SIDENOTE: order only applies to attributes and not to instance validators).
Validators
Currently there are 7 validators. Each validator has a corresponding attribute to use. In addition, each attribute has static methods that facilitate the creation of itself programatically.
Compare Validator
Applicable to types that implement IComparable
.
Tests by comparing the instance value with a constant value using a specified comparison operator.
...
[CompareValidator(ComparisonOperator.GreaterThan,0)]
public int TestProperty
{
get { return this.testProperty; }
set { this.testProperty = value; }
}
...
Custom Validator
Applicable to any type.
Takes a method name as a parameter that exists in the type. The method must conform to the Predicate<T>
delegate signature.
...
[CustomValidator("TestProperty cannot contain a period.","NoPeriodTest")]
public string TestProperty
{
get { return this.testProperty; }
set { this.testProperty = value; }
}
private bool NoPeriodTest(string value)
{
return !value.Contains(".");
}
...
Length Validator
Applicable to System.String
and types that implement ICollection
.
Tests by ensuring that the string's length or collection's count falls within the length restriction.
...
[LengthValidator(1,10)]
public string TestProperty
{
get { return this.testProperty; }
set { this.testProperty = value; }
}
...
NotEmpty Validator
Applicable to types that implement IEnumerable
.
Tests by ensuring that there is at least 1 element to enumerate.
...
[NotEmptyValidator]
public string TestProperty
{
get { return this.testProperty; }
set { this.testProperty = value; }
}
...
Range Validator
Applicable to types that implement IComparable
.
Tests by checking whether the value falls within the minvalue and the maxvalue. [inclusively].
...
[RangeValidator(1,10)]
public int TestProperty
{
get { return this.testProperty; }
set { this.testProperty = value; }
}
...
Regular Expression Validator
Applicable to strings.
Tests by checking whether the value matches the regular expression.
...
[RegexValidator(@"\d+")]
public string TestProperty
{
get { return this.testProperty; }
set { this.testProperty = value; }
}
...
Required Validator
Applicable to any type. However, value types such as int
will always pass validation. Nullable types work as expected.
Tests for null
. Therefore, while it is applicable to value types, it will always return true
. Nullable types behave normally.
...
[RequiredValidator]
public string TestProperty
{
get { return this.testProperty; }
set { this.testProperty = value; }
}
...
Stuff Left to Do
I feel the library is fairly complete. There are 25 unit tests utilizing the NUnit framework. (I am by no means a unit test writer, so don't laugh at them.) The one thing I do wish to do is incorporate another way of registering validators with the framework. I am pursuing doing that through the app/web config files. This would completely decouple business logic validation from the domain model.
In addition, I'm not sure if the library is thread-safe. I think it is, because the underlying dictionary objects are thread-safe, but I'm not an expert in this area and will defer to another's judgement.
Finally, I'm still debating whether or not to allow the removal of validators at run-time from types and/or instances. I have not required this functionality, but perhaps others may find it useful.
Interesting Properties
Something I realized later, after having worked on the framework for some time, is that there is an inversion of control property associated with the framework. For instance, we can pass a ValidationManager
and instance of any object, register some validators, and assure ourselves of its validity. For example:
ArrayList list = new ArrayList();
ValidationManager vm = new ValidationManager(list);
vm.AddValidator(NotEmptyValidator.CreateValidator(typeof(ArrayList),"Count"));
Assert.IsFalse(vm.IsValid());
list.Add(2);
Assert.IsTrue(vm.IsValid());
Obviously, this is a trivial example, but the illustration is made.
Conclusion
We have created a formal validation framework that is extensible and easy to use. I hope you enjoyed the article, but hope that you enjoy using the library even more. Please keep me in the loop regarding code changes/enhancements/bug fixes that you make.