April 2012 - Posts

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

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.

It’s time to dive into a big subject: the business rules on individual DataFields (properties defined in the Entity class). Business rules include validation, declaring the real-world data type, and so much more as shown in the chart below. Like any good object oriented implementation, business rules are described using objects. Microsoft introduced the approach: DataAnnotation attributes for its ASP.NET Dynamic Data, Silverlight, and MVC offerings.

To review, Attributes apply metadata to properties and classes. They are created by subclassing System.Attribute. They are used by adding a language specific syntax above the declaration of a class, property, method, etc. In the case of applying business rules to DataFields, these Attributes are applied to properties within the Entity class and Entity metadata class.

In this example, you see the RequiredAttribute, StringLengthAttribute, and CustomValidationAttribute, all supplied in the .net framework’s System.ComponentModel.DataAnnotations namespace.

[C#]

public class CategoryMetadata
{
   [Required()]
   [StringLength(30)]
   [CustomValidation(MethodName="CheckForDuplicateCategoryNames")]
   public object CategoryName { get; set; }
}

[VB]

Public Class CategoryMetadata
   <Required()> _
   <StringLength(30)> _
  <CustomValidation(MethodName := "CheckForDuplicateCategoryNames")> _
  Public Property CategoryName As Object
    Get
      Return Nothing
    End Get
    Set
    End Set
  End Property
End Class

Despite being shown here as metadata, business rules really don’t care where they come from. It’s perfectly fine to create the RequiredAttribute programmatically instead of attaching it to a property. BLD provides a mechanism to do this through its DataFieldDescriptor objects which host all business rules. As DataFieldDescriptors are created, they either get the DataAnnotation attributes from metadata or your event handler which creates them programmatically. DataAnnotation attributes can also be created as the situation demands. See “Customizing Business Rules at runtime”.

Let’s boil it down to this:

  • DataAnnotation attributes are objects which host values for business rules.
  • You can create them.
  • Microsoft has created a few of them for you.

BLD has also created some for you. Let’s introduce you to the many predefined business rules you have when using BLD.

Validation rules – When you think of business logic, validation comes to mind. The .net framework gets us started with a standard way to create validation rules, by subclassing System.ComponentModel.DataAnnotations.ValidationAttribute.

Due to DES’s very powerful Validation Framework, BLD introduces richer versions of those supplied in .net, and adds many more. See “Understanding ValidationAttributes”.

Even though they supply validation, ValidationAttributes can offer other features. For example:

  • Use the DESDA.InjectionSecurityAttribute to determine illegal content supplied by hackers, log it, and block further access to the page. (Normally validation only reports back to the user an issue.)
  • Use the DESDA.CharacterSetAttribute not only to define the character set allowed by a string-type field, but also to inform the TextBox control how to create keyboard filtering rules to match.

Data types – Each property has a type, such as string, integer or boolean, that mimics the type from the Column in the table (nvarchar, integer). That type is often not specific enough for developing a user interface or validation rules. For example, a double can be used as a currency, percentage, duration, longitude, etc. DataTypeAttributes let you redefine the type and provide more specific validation and formatting rules.

Once again, the .net framework gets us started with a standard way to describe a data type: the System.ComponentModel.DataAnnotations.DataTypeAttribute. BLD handles many new cases, like Measurement, DbImage, Duration, and Enumerated. These attributes have a rich set of tools to create your business rules. Most DataTypeAttributes have validation rules too. For example, the DESDA.EmailAddressAttribute creates a regular expression for the email address pattern.

See “Understanding DataTypeAttributes”.

Textual rules – Provide user friendly and localized names for fields, default strings for descriptions (such as used in a tooltip or hint) and for prompts with the DESDA.DisplayNameAttribute and DESDA.DescriptionAttribute.

Data value rules – Provide a default value for the DataField when creating a new record with the DefaultValueAttribute. Provide string formatting rules (to convert a date or number) with the DisplayFormatAttribute. (Both of these are from System.ComponentModel.DataAnnotations.)

Security rules – Associate restrictions based on user roles to Entity classes and DataFields by adding the DESDA.TableRestrictionAttribute and DESDA.ColumnRestrictionAttribute to your business logic. The user interface changes as a restricted feature is requested, whether to hide the feature or provide an alternative. (The DESDA.TableRestrictionAttribute is an example of a DataAnnotation attribute that goes on the Entity class itself, not a DataField. DESDA.DisplayNameAttribute and a few others can also provide business rules this way.)

Calculation rules – Use the DESDA.CalculationDescriptorAttribute to describe the underlying calculation on a property. The user interface can interpret it to create interactive calculations on screen.

Foreign key rules – Use the DESDA.ForeignKeyQueryAttribute to define filters and sort order on the lists that show elements of a foreign table. To avoid querying the same data every time, establish caching requirements with the DESDA.CacheListAttribute.

Dependency rules – Marks a DataField as dependant on another with a specific rule defined with a DependencyAttribute. Dependencies enable validation rules, such as a field is only required when another field has a value. The user interface can also use dependencies to change the appearance of, or hide, a field based on the dependency’s rules.

Sorting rules – Use the DESDA.SortableColumnAttribute to identify if a DataField can be sorted or not. The user interface uses this to establish its sorting interface, such as column titles that change sort order when clicked.

Scaffolding rules – “Scaffolding” allows a user interface to be populated with DataFields automatically, using the same layout for each DataField shown. The DESDA.ScaffoldColumnAttribute indicates if the DataField is shown when using Automatic Scaffolding and in what order to place it.

DataField categories – Use the System.ComponentModel.CategoryAttribute on DataFields to assign one or more category names with the DataField. BLD’s user interface controls support categories with Automatic Scaffolding and to select a custom CSS class or style for an element of its Template (called “Named Styles”).

Filtering rules – The DESDA.FilterAttribute determines if and when a DataField can be added to a filtering user interface. The DESDA.RangeEntityFilterPickerAttributes describe pick lists of values for numeric and date DataFields seen so often in sales websites (like pick a price range: $0-$19, $20-$49, $50-$99.) See “FilterAttributes”.

Even with over 50 DataAnnotation attributes defined between BLD and the .net framework, this doesn’t cover all cases. You should create anything you don’t find here.

In the next few topics, we’ll look more carefully at some of the attribute classes that you may want to subclass for expansion.

More Posts