Monday, December 7, 2009 12:31 PM plblum

The CustomValidationAttribute

.net 4 introduces the CustomValidationAttribute, a member of System.ComponentModel.DataAnnotations that supports validation of your business logic. Until now, validation rules were limited based on the available attributes (required, stringlength, regex, and range) plus those you created by subclassing System.ComponentModel.DataAnnotations.ValidationAttribute. CustomValidationAttribute lets you define new validation rules directly on your Entity class (or the class of your choice) without having to subclass from ValidationAttribute.

Unfortunately at this time, the documentation on this attribute is limited. This posting is an attempt to document it better.

The CustomValidationAttribute can validate an individual property or the complete object (the Entity instance). This is an example using the Category table from Northwind DB and LINQ to SQL.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using System.Text.RegularExpressions;

[MetadataType(typeof(CategoryMetadata))]
[CustomValidation(typeof(Category), "FinalCheck")]
public partial class Category
{
public class CategoryMetadata
{
[CustomValidation(typeof(Category), "TestCategoryName")]
public object CategoryName { get; set; }
}

// method defined by LINQ to SQL on every Entity class.
partial void OnValidate(System.Data.Linq.ChangeAction action)
{
List<ValidationResult> vErrors = new List<ValidationResult>();

// validate class-level attributes
// When the last parameter is true, it also evaluates all ValidationAttributes on this entity's properties
Validator.TryValidateObject(this, new ValidationContext(this, null, null), vErrors, true);

// handle errors
if (vErrors.Count > 0)
throw new ValidationException(vErrors[0], null, this);

}

public static ValidationResult FinalCheck(Category pCategory, ValidationContext pValidationContext)
{
if (pCategory.Description.Length < 10)
return new ValidationResult("Description must be at least 10 characters.", new List<string> { "Description" });
return ValidationResult.Success;
}

public static ValidationResult TestCategoryName(string pNewName, ValidationContext pValidationContext)
{
if (Regex.IsMatch(pNewName, @"^\d")) // cannot start with a digit
return new ValidationResult("Cannot start with a digit", new List<string> { "CategoryName" });
return ValidationResult.Success;
}
}

 

Understanding the CustomValidatorAttribute definition

  • It can be placed on the class or property definition. Both are shown above.
  • The class that contains your validation method must always be declared, even if the method is in the same class this attribute is assigned.
  • You may want to create a separate class for all validation rules of your entity, such as “class CategoryValidation”.
  • The method name property must be a case sensitive match your method name.

Understanding the method definition

  • The method itself must be static and public.
  • The first parameter is the value to evaluate.
    • When the method is associated with the class, it will be passed an instance of the Entity. It can be typed to match the class of the Entity, such as it is with “Category” above. It can also be typed to “object”.
    • When the method is associated with a property, it will be passed the value of the property. It can be typed to match the type of the property, such as it is with “string” above. It can also be typed to “object”.
  • The second parameter, ValidationContext, describes other details about the activity. It’s more important when validating a property since it supplies the instance of the Entity (ObjectInstance property) and the property name (MemberName property). It’s also possible that this parameter will be null, but that generally occurs when using .net 3.5 libraries. So test for that.
  • Always return an instance of System.ComponentModel.DataAnnotations.ValidationResult. When there is no error, just return the static property ValidationResult.Success, as shown above. Otherwise return a description and optionally the property name(s) with errors.

Invoking Validation

Your CustomValidation methods are not called automatically (despite what the documentation presently says in the “Remarks” section). ASP.NET Dynamic Data knows to evaluate the property-based ValidationAttributes when there is a DynamicValidator control on the page. But since your Entity class may be reused, do not assume that Dynamic Data has done the job. Create a method on your Entity class to validate both class and property-level CustomValidationAttributes. Use System.ComponentModel.DataAnnotations.Validator.TryValidateObject() to do the job, as is shown above. Always pass true as the last parameter to insure the properties are validated. (If you pass false or omit the parameter, it will only validate the RequiredAttributes on properties.)

The OnValidate() method above is part of LINQ to SQL. Each Entity class has a partial method called OnValidate(). You create the other partial method in your code and LINQ to SQL will call it automatically. If you are using Entity Framework or another system, you must create and invoke your own “OnValidate()” method.

Communicating Errors to the User Interface

Your error messages from the ValidationResult class somehow have to be shown in the user interface. A method like OnValidate() has no direct way to pass up the list of ValidationResults. Instead, it throws a System.ComponentModel.DataAnnotations.ValidationException. (Unfortunately, it does not pass the list of ValidationResults in that exception class. You may want to build another Exception class to deliver them.)

The user interface should catch the exception and route the messages to its error display. When using ASP.NET with Dynamic Data, make sure you have a ValidationSummary and DynamicValidator control on the page, and Dynamic Data will take care of the rest. The Page Templates are setup this way.


<asp:ValidationSummary ID="ValidationSummary1" runat="server"
HeaderText="List of validation errors" />
<asp:DynamicValidator runat="server" ID="DetailsViewValidator"
ControlToValidate="FormView1" Display="None" />
<asp:FormView runat="server" ID="FormView1" >

 

Suppose you are creating a new Category and enter a name starting with a digit. Your validation rules will report an error like this:

clip_image002

Filed under: , , , ,

Comments

# re: The CustomValidationAttribute

Monday, April 12, 2010 8:17 PM by Lance

Thanks for the great post. I didn't know about the last parameter of the TryValidateObject. Now I can validate my custom validation.

I have one question. The membername property of the validation error is empty for the custom validation whereas it contains the property name for the standard validators. Any idea on how to get the membername of a custom validation error?

# re: The CustomValidationAttribute

Wednesday, November 24, 2010 5:32 AM by Cheap Flower Guy

@Lance: this appears to be a bug. It should pick up the membername from the ValidationResult overload that takes in an IEnumerable<string> of member names but this is not happening. This means that whilst it displays the error when using a ValidationSummary, it will not be displayed if using control level validators (ie Html.ValidationMessageFor in MVC)