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.

ValidatorObject

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:

ValidatorObject construction

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

No Comments