Digging into ValidationAttributes
This series looks at the Business Logic Layer of an application from how my Peter’s Business Logic Driven UI (“BLD”) implements it. Use the “Understanding the BLL” Category to see all articles.
Validation is the most prominent business rule. I have been building validation objects for years with my Peter’s Data Entry Suite which is for ASP.NET Web Forms. BLD is actually a module of Peter’s Data Entry Suite, using my “DES Validation Framework” for the validation infrastructure of both the Business Logic and User Interface Layers. It was inspired by the work done by Microsoft for ASP.NET 3.5, which included the System.ComponentModel.DataAnnotation namespace and ASP.NET Dynamic Data. It was also inspired by my customers who wanted some way to define validation rules in the Business Logic Layer.
Given my 10+ years of experience around validation, I had a different level of expectations for what a “Validation DataAnnotation attribute” should do than what came with Microsoft’s System.ComponentModel.DataAnnotations.ValidationAttribute. So I built a better ValidationAttribute.
Here’s a key concept: the ValidationAttribute should not house the logic that evaluates some value and returns “Success” or “Failed”. Instead, you should have objects designed around that evaluation which can work in numerous places, not just in a business logic layer, such as a Range validator object, Compare to value validator object, and Regular expression validator object. The ValidationAttribute is merely a front end that knows how to create one of your validation objects and if needed, invoke its evaluation code in its Validate() method.
I’ll dig into how the Validator object is constructed later in this post.
ValidationAttributes are a thin layer around Validator objects
The ValidationAttribute is a thin layer that has the same properties found on the actual Validator object. It contains two vital methods: IsValid() and PrepareValidator().
IsValid() is defined by the base class and is used by the Business Logic Layer to validate.
PrepareValidator() creates a validator control for use by the UI Layer. The following interface should be added to expose PrepareValidator():
[C#]
public interface IPrepareValidatorForUI
{
IValidator PrepareValidator(DataTypeDescriptor dataTypeDescriptor, ContextInfo contextInfo);
}
[VB]
Public Interface IPrepareValidatorForUI
Function PrepareValidator(dataTypeDescriptor As DataTypeDescriptor, contextInfo As ContextInfo) As IValidator
End Interface
- IValidator is an interface applied to all validator objects. It has methods for Validate(), GetErrorMessage(), GetSummaryErrorMessage() and these properties: IsValid, Enabled, HasValidated, and CultureInfo. BLD’s BaseValidatorAction class and each DES Validator web control implement it. Any validators you create using BLD for another UI Layer should implement it.
- DataTypeDescriptor describes all kinds of details about a single DataField (property on the Entity class.) See “Descriptor objects : Exposing the structure and business logic to consumers”.
- ContextInfo lets the UI layer identify itself so PrepareValidator() can create the correct class for the validator through a factory. Each UI Layer registers its validator classes with that factory so PrepareValidator() (which is in the BLL) never knows its dealing with a class defined in the UI Layer.
(In BLD, I have implemented this differently. The PrepareValidator() method and this interface simplify things to show the concept. If you are using BLD, explore the PeterBlum.DES.DataAnnotations.IDESValidationAttribute interface.)
Here is pseudocode for a RangeAttribute that uses the RangeValidator object defined elsewhere.
[C#]
using System.ComponentModel.DataAnnotations.ValidationAttribute;
…
public class RangeAttribute : ValidationAttribute, IPrepareValidatorForUI
{
public string SummaryErrorMessage { get; set; } // base class defines ErrorMessage property
public object MinimumAsNative { get; set; }
public object MaximumAsNative { get; set; }
public virtual IValidator PrepareValidator(BaseDataFieldDescriptor dataFieldDescriptor, ContextInfo contextInfo)
{
IRangeValidator vValidator = ValidatorFactory.Create<IRangeValidator>(contextInfo);
vValidator.ErrorMessage = this.ErrorMessage;
vValidator.SummaryErrorMessage = this.SummaryErrorMessage;
vValidator.MinimumAsNative = this.MinimumAsNative;
vValidator.MaximumAsNative = this.MaximumAsNative;
vValidator. TypeConverter = CreateTypeConverter(dataFieldDescriptor);
return vValidator;
}
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
// caller is expected to have added DataFieldDescriptor and ContextInfo to validationContext.Items
BaseDataFieldDescriptor vDataFieldDescriptor = GetDataFieldDescriptor(validationContext);
ContextInfo vContextInfo = GetContextInfo(validationContext);
IValidator vValidator = PrepareValidator(vDataFieldDescriptor, vContextInfo);
if (vValidator.Validate())
return ValidationResult.Success;
return new EntityValidationResult(this, vValidator.GetErrorMessage(true),
validationContext.ObjectType, validationContext.MemberName, value);
}
}
[VB]
Imports System.ComponentModel.DataAnnotations.ValidationAttribute
…
Public Class RangeAttribute
Inherits ValidationAttribute
Implements IPrepareValidatorForUI
' base class defines ErrorMessage property
Public Property SummaryErrorMessage() As String
...
End Property
Public Property MinimumAsNative() As Object
...
End Property
Public Property MaximumAsNative() As Object
...
End Property
Public Overridable Function PrepareValidator(
dataFieldDescriptor As BaseDataFieldDescriptor, contextInfo As ContextInfo) As IValidator
Dim vValidator As IRangeValidator = ValidatorFactory.Create(Of IRangeValidator)(contextInfo)
vValidator.ErrorMessage = Me.ErrorMessage
vValidator.SummaryErrorMessage = Me.SummaryErrorMessage
vValidator.MinimumAsNative = Me.MinimumAsNative
vValidator.MaximumAsNative = Me.MaximumAsNative
vValidator.TypeConverter = CreateTypeConverter(dataFieldDescriptor)
Return vValidator
End Function
Protected Overrides Function IsValid(
value As Object, validationContext As ValidationContext) As ValidationResult
' caller is expected to have added DataFieldDescriptor and ContextInfo to validationContext.Items
Dim vDataFieldDescriptor As BaseDataFieldDescriptor = GetDataFieldDescriptor(validationContext)
Dim vContextInfo As ContextInfo = GetContextInfo(validationContext)
Dim vValidator As IValidator = PrepareValidator(vDataFieldDescriptor, vContextInfo)
If vValidator.Validate() Then
Return ValidationResult.Success
End If
Return New EntityValidationResult(Me, vValidator.GetErrorMessage(True),
validationContext.ObjectType, validationContext.MemberName, value)
End Function
End Class
Please keep in mind the above is not the real code in BLD, nor will it compile and work.
Let’s look at this more carefully.
- The base class, System.ComponentModel.DataAnnotations.ValidationAttribute, defines ErrorMessage and localization support for it. The pseudocode doesn’t go into the details of localization, but assume the above code applies it.
- All Validator objects should declare an interface unique to it that is applied to the validator control for each UI layer. In this case, I’ve used IRangeValidator. Assume it’s applied to the RangeValidator control defined in Web Forms, MVC, Silverlight, etc. The ValidatorFactory.Create<TValidator>() method uses that interface to ask a factory to create the appropriate validator control. Each new UI layer should create a set of supported Validator controls and register them with the factory. (BLD has done this for Web Forms and a UI neutral form.)
- Many validators need to know the type of the data to be evaluated. They use a specialized Type Converter object to ensure the data passed is correct. The CreateTypeConverter() method handles this, getting the type of data from the DataFieldDescriptor object. The Type Converter is described later in this post.
- The IsValid() method is declared on the ValidationAttribute class and must be overridden to supply the validation used by the Business Logic Layer. EntityValidationResult is a subclass of System.ComponentModel.DataAnnotations.ValidatonResult that contains the actual ValidationAttribute. With the ValidationAttribute passed back, the UI Layer has enough details to show ErrorMessage text next to the actual field with the error and the SummaryErrorMessage to the ValidationSummary control.
How a Validator object is constructed
When I designed the Validator web controls for Peter’s Data Entry Suite, I identified a number of unique classes that must come together to complete the control. The main elements of a validator do not involve the user interface and can be reused in many places. Here is a look at those classes:
Of these classes, the Condition is the most important and most versatile. So let’s define it first.
Condition classes
Condition classes evaluate something about your page and return “success”, “failed”, or “cannot evaluate”. They are the heart of validation. Each time you have a validation rule, you create a new Condition class. DES has over 30 of them.
Here is a very stripped down version of the base condition class.
[C#]
public class BaseCondition
{
public bool Enabled { get; set; }
public virtual bool CanEvaluate() { }
public virtual int EvaluateCondition() {}
}
[VB]
Public Class BaseCondition
Public Property Enabled As Boolean
…
End Property
Public Overridable Function CanEvaluate() As Boolean
…
End Function
Public Overridable Function EvaluateCondition () As Integer
…
End Function
End Class
A Condition is used for much more than just validation. It effectively lets you create “if then” logic. For example, “if CheckBox1 is checked, then validate.” This case is implemented by adding a new property to your Validator control which DES calls the “Enabler”. This property is of type Condition. Here’s DES’s RangeValidator web control in markup using the CheckStateCondition which handles “if CheckBox1 is checked”.
<des:RangeValidator id="RangeValidator1" runat="server" ErrorMessage="Must be between {MINIMUM} and {MAXIMUM}"
Minimum="1" Maximum="10" DataType="Integer" ControlIDToEvaluate="TextBox1">
<EnablerContainer>
<des:CheckStateCondition ControlIDToEvaluate="CheckBox1" Checked="true" />
</EnablerContainer>
</des:RangeValidator>
DES employs the Enabler property on several of its controls.
Another great usage of the Condition involves building a validator with complex boolean logic. Suppose you want to validate “TextBox1 has text AND TextBox2 has text”. This is really a boolean expression using the AND operator and two RequiredTextConditions. DES’s MultiConditionValidator lets you construct these boolean expressions. Here’s markup for the above logic.
<des:MultiConditionValidator id="MCV1" runat="server" Operator="And" ErrorMessage="Both must have text">
<Conditions>
<des:RequiredTextCondition ControlIDToEvaluate="TextBox1" />
<des:RequiredTextCondition ControlIDToEvaluate="TextBox2" />
</Conditions>
</des:MultiConditionValidator>
If you like, explore several more cases implemented in DES’s CalculationController and FieldStateController controls.
Type Converter classes
The .net framework defines the System.ComponentModel.TypeConverter class as a way to convert data between two forms, usually between string and a native type. This is very important to allow users to evaluate data that isn’t the expected (native) type. DES has declared its own implementation of this concept (based on PeterBlum.DES.DESTypeConverter) to provide a richer set of options for converting between strings and native types. For example, the CurrencyTypeConverter has properties to allow a currency symbol, thousands separator, and extra decimal digits.
The BaseCondition class can be assigned a DESTypeConverter object to its DESTypeConverter property. Once setup, any string you want the Condition to evaluate will be processed first by the DESTypeConverter. If conversion to the native type is successful, the evaluation continues. If it fails, most Conditions report back “cannot evaluate” to the caller.
ValidatorAction classes
The ValidatorAction class owns a Condition object and knows how to use it to validate.
It has the following features:
- Error message properties. There are two cases: shown at the location of the Validator UI element and shown in a ValidationSummary list. Error messages can support tokens specific to the Validator class, such as “{MINIMUM}” and “{MAXIMUM}” previously shown on the RangeValidator.
- Enabler property to use a Condition to enable the validator based on the Condition’s rule.
- The Validate() method, IsValid property, and validation group name property. The Validate() function checks the validation group name passed into the function, the Condition’s CanEvaluate() method, and the Enabler condition before attempting to validate. To evaluate it calls the Condition’s EvaluateCondition() method and sets IsValid based on the result.
Here is a very stripped down version of the base ValidatorAction class.
[C#]
public class BaseValidatorAction
{
protected IBaseCondition Condition { get; }
public IBaseCondition Enabler { get; set; }
public virtual bool CanDoAction()
{
// checks the Enabler and Condition.CanEvaluate()
}
public string ErrorMessage { get; set; }
public string SummaryErrorMessage { get; set; }
public virtual string GetErrorMessage()
{
// replaces tokens on ErrorMessage and returns the result
}
public virtual string GetSummaryErrorMessage()
{
// replaces tokens on SummaryErrorMessage and returns the result
}
public string Group { get; set; }
public bool IsValid { get; set; } // defaults to true
public virtual bool Validate(string group)
{ // pseudocode
IsValid = true;
if ((group != Group) || !CanDoAction())
return true;
IsValid = Condition.EvaluateCondition() != 0;
return IsValid;
}
public virtual bool Validate()
{ // pseudocode
IsValid = true;
if (!CanDoAction())
return true;
IsValid = Condition.EvaluateCondition() != 0;
return IsValid;
}
}
[VB]
Public Class BaseValidatorAction
Protected Property Condition As IBaseCondition
…
End Property
Public Property Enabler As IBaseCondition
…
End Property
Public Overridable Function CanDoAction() As Boolean
' checks the Enabler and Condition.CanEvaluate()
End Function
Public Property ErrorMessage As String
…
End Property
Public Property SummaryErrorMessage As String
…
End Property
Public Overridable Function GetErrorMessage() As String
' replaces tokens on ErrorMessage and returns the result
End Function
Public Overridable Function GetSummaryErrorMessage() As String
' replaces tokens on SummaryErrorMessage and returns the result
End Function
Public Property Group As String
…
End Property
Public Property IsValid As Boolean ' defaults to true
…
End Property
Public Overridable Function Validate(group As String) As Boolean ' pseudocode
IsValid = True
If (group <> Group) Or Not CanDoAction() Then
Return True
End If
IsValid = Condition.EvaluateCondition() <> 0
Return IsValid
End Function
Public Overridable Function Validate(group As String) As Boolean ' pseudocode
IsValid = True
If Not CanDoAction() Then
Return True
End If
IsValid = Condition.EvaluateCondition() <> 0
Return IsValid
End Function
End Class
ErrorFormatter classes
Now we get into the UI layer which means these classes must be recreated for each UI framework (BLD already handles Web Forms). The job of the UI layer validator control is to invoke the ValidatorAction object’s Validate() method and create whatever presentation is needed for communicating the error at the location of the control. There are many ways to present an error message. The native ASP.NET Web Forms validator controls do it with a <span> tag containing the error message. This inserts text into the page. You might prefer a popup element, a tooltip, or an alert to show the error.
The ErrorFormatter class defines each way to present the error message to the user. The user can add a Validator control and select the desired UI in the ErrorFormatter property.
For example:
<des:RequiredTextValidator id="RTV" runat="server" ControlIDToEvaluate="TextBox1" ErrorMessage="Required" >
<ErrorFormatterContainer>
<des:PopupErrorFormatter />
</ErrorFormatterContainer>
</des:RequiredTextValidator>
You can check out the 5 ErrorFormatter classes included with DES here.
Validator control class
Finally we get to the object actually added to the UI layer. It’s really a lightweight object exposing all of the properties and methods on Validator, Condition, and TypeConverter objects that describe the validation rule.
This is pseudocode for the base Validator web control class:
[C#]
public class BaseValidator : WebControl
{
protected BaseValidatorAction ValidatorAction
{
get
{
if (fValidatorAction == null)
fValidatorAction = CreateValidatorAction();
return fValidatorAction;
}
}
private BaseValidatorAction fValidatorAction;
protected abstract BaseValidatorAction CreateValidatorAction();
public BaseErrorFormatter ErrorFormatter
{
get { return fErrorFormatter; }
set { fErrorFormatter = value; }
}
private BaseErrorFormatter fErrorFormatter;
public bool IsValid
{
get { return ValidatorAction.IsValid; }
}
public bool Validate(string group)
{ return ValidatorAction.Validate(group); }
public string ErrorMessage
{
get { return ValidatorAction.ErrorMessage; }
set { ValidatorAction.ErrorMessage = value; }
}
public string SummaryErrorMessage
{
get { return ValidatorAction.SummaryErrorMessage; }
set { ValidatorAction.SummaryErrorMessage = value; }
}
public bool Enabled
{
get { return ValidatorAction.Condition.Enabled; }
set { ValidatorAction.Condition.Enabled = value; }
}
}
[VB]
Public Class BaseValidator
Inherits WebControl
Protected ReadOnly Property ValidatorAction() As BaseValidatorAction
Get
If fValidatorAction Is Nothing Then
fValidatorAction = CreateValidatorAction()
End If
Return fValidatorAction
End Get
End Property
Private fValidatorAction As BaseValidatorAction
Protected MustOverride Function CreateValidatorAction() As BaseValidatorAction
Public Property ErrorFormatter As BaseErrorFormatter
Get
Return fErrorFormatter
End Get
Set
fErrorFormatter = value
End Set
End Property
Private fErrorFormatter As BaseErrorFormatter
Public ReadOnly Property IsValid() As Boolean
Get
Return ValidatorAction.IsValid
End Get
End Property
Public Function Validate(group As String) As Boolean
Return ValidatorAction.Validate(group)
End Function
Public Property ErrorMessage() As String
Get
Return ValidatorAction.ErrorMessage
End Get
Set
ValidatorAction.ErrorMessage = value
End Set
End Property
Public Property SummaryErrorMessage() As String
Get
Return ValidatorAction.SummaryErrorMessage
End Get
Set
ValidatorAction.SummaryErrorMessage = value
End Set
End Property
Public Property Enabled() As Boolean
Get
Return ValidatorAction.Condition.Enabled
End Get
Set
ValidatorAction.Condition.Enabled = value
End Set
End Property
End Class