There are a lot of good and free frameworks to help you deal with validating user-submitted data. You can use attribute-based frameworks, such as Castle Validators or .NET 3.5 Data Annotations to decorate your objects with simple rules, and then leverage frameworks such as xVal or the MVC Validation Toolkit to enforce those attribute both on the client and server side. (For those who dislike using attributes in this manner, you could also use something like FluentValidation).
Despite these frameworks, there may be situations where you need to roll your own, which is what we're going to look at in this series. We won't be doing anything fancy, but we will look at an end-to-end custom solution.
Attributes
The first thing we need to do is decide how we'll define our rules. We'll pick a mix of attributes for simple validation cases, and an interface for more advanced/custom validation. Our goal is to end up with something like:
public interface IValidate
{
ValidationError[] Validate(IRepository repository);
}
public class User : IValidate
{
private string _email;
private string _password;
[Required, StringLength(50), Pattern(".+@.+\\..+"), Tip("Please enter a valid email, a confirmation email will be sent.")]
public string Email
{
get { return _email; }
set { _email = value; }
}
[Required, StringLength(4, 30), Tip("Please enter your password. 4-30 characters")]
public string Password
{
get { return _password; }
set { _password = value; }
}
public virtual ValidationError[] Validate(IRepository repository)
{
if (repository.Exists<User>(u => u.Email == Email))
{
return new[] {new ValidationError("Email", "This email is already registered")};
}
return null;
}
}
We could always externalize the "Tips" for localization (or cleanliness) purposes, but this keeps everything rather simple. (If you're put off by the attributes, check outFluentValidation for an idea on how else you migth define your rules).
The foundation for our attributes is a simple base class:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public abstract class BaseValidatorAttribute : Attribute
{
public abstract IDictionary<string, string> ToJson();
public abstract bool IsValid(object value);
}
And here's the simplest implementation:
public class RequiredAttribute : BaseValidatorAttribute
{
public override IDictionary<string, string> ToJson()
{
return new Dictionary<string, string> {{"required", "true"}};
}
public override bool IsValid(object value)
{
if (value == null)
{
return false;
}
return !(value is string) || ((string)value).Length != 0;
}
}
The IsValid method is used to validate the object server-side. The ToJson method is used to generate a javascript object for client-side use.
Let's take a look at the StringLengthAttribute - which is slightly more complicated:
public class StringLengthAttribute : BaseValidatorAttribute
{
private readonly int _minimumLength;
private readonly int _maximumLength;
public int MinimumLength
{
get { return _minimumLength; }
}
public int MaximumLength
{
get { return _maximumLength; }
}
public StringLengthAttribute(int maximumLength) : this(0, maximumLength){}
public StringLengthAttribute(int minimumLength, int maximumLength)
{
_minimumLength = minimumLength;
_maximumLength = maximumLength;
}
public override IDictionary<string, string> ToJson()
{
return new Dictionary<string, string>
{
{ "min", _minimumLength.ToString() },
{ "max", _maximumLength.ToString() },
};
}
public override bool IsValid(object value)
{
if (!(value is string))
{
return false;
}
var length = ((string)value).Length;
return length >= _minimumLength && length <= _maximumLength;
}
}
There really is no limit to what you're able to do with these attributes. You can build groups of validation rules (each group might have its own tip), compare validators, or even validators that hit the DB, such as a UniqueAttribute (although we've opted to implement that via the IValidate interface).
ValidatorConfiguration
There are two problems we'll run into by using attributes - they are somewhat cumbersome to program against, and they can impact performance. Since validation is static, we can parse and cache more useful structures which we can then use when doing our actual client-side and server-side validation. We'll use two classes to represent our attributes:
public class EntityValidationInfo
{
public IEnumerable<PropertyValidationInfo> Properties { get; private set; }
public EntityValidationInfo(IEnumerable<PropertyValidationInfo> properties)
{
Properties = properties;
}
}
public class PropertyValidationInfo
{
public IEnumerable<BaseValidatorAttribute> Validators { get; private set; }
public PropertyInfo Property { get; private set;}
public PropertyValidationInfo(PropertyInfo property, IEnumerable<BaseValidatorAttribute> validators)
{
Property = property;
Validators = validators;
}
}
The purpose behind these is that given an object type, we can lookup its EntityValidationInfo which contains a collection of properties and validators. Next we'll create a ValidatorConfiguration class with a couple static members:
public class ValidatorConfiguration
{
private static IDictionary<Type, EntityValidationInfo> _rules;
public static IDictionary<Type, EntityValidationInfo> Rules
{
get { return _rules; }
}
public static void Initialize(params string[] assemblyNames)
{
//todo
}
}
Our goal is to initialize ValidatorConfiguration once, on startup, and then access its Rules property as needed. The Initialize method takes in assembly names to scan for classes using validation attributes (you might have these spread out across multiple assemblies). In an ASP.NET application, we'd use the Application_Start event and use something like:
ValidatorConfiguration.Initialize("MyAssembly", "MyAssembly.Web");
Finally, the implementation of Initialize looks like:
public static void Initialize(params string[] assemblyNames)
{
_rules = new Dictionary<Type, EntityValidationInfo>(10);
foreach (var assemblyName in assemblyNames)
{
foreach (var kvp in LoadFromAssembly(Assembly.Load(assemblyName)))
{
_rules.Add(kvp.Key, kvp.Value);
}
}
}
private static IDictionary<Type, EntityValidationInfo> LoadFromAssembly(Assembly assembly)
{
var rules = new Dictionary<Type, EntityValidationInfo>(10);
foreach (var type in assembly.GetExportedTypes())
{
var info = GetEntityValidationInfo(type);
if (info != null) { rules.Add(type, info); }
}
return rules;
}
private static EntityValidationInfo GetEntityValidationInfo(Type type)
{
var properties = new List<PropertyValidationInfo>();
foreach (var property in type.GetProperties())
{
var attributes = (BaseValidatorAttribute[])property.GetCustomAttributes(typeof(BaseValidatorAttribute), true);
if (attributes.Length == 0) { continue; }
properties.Add(new PropertyValidationInfo(property, attributes));
}
return properties.Count == 0 ? null : new EntityValidationInfo(properties);
}
There isn't much magic going on here. We loop through each property, of each public type of each assembly, looking for validation attributes. If we don't find any, we simply move on to the next. If we do find them, we create new instances of our EntityValidationInfo and PropertyValidationInfo and associate that to a Type within the static _rules field.
Conclusion
We've managed to lay the foundation for actually being able to validate stuff. In Part 2 we'll look at client-side validation, and in Part 3 we'll cover server-side validation. Stay tuned.
Posted
Sun, Apr 26 2009 3:17 PM
by
karl