ASP.NET Dynamic Data introduced the System.ComponentModel.DataAnnotations namespace in .NET 3.5 SP1. The namespace contained a bunch of attributes for applying validation rules to objects and their properties. With the “Alexandria” project (which morphed into .NET RIA Services plus some Silverlight/SDK/Toolkit additions), we were exposing your server-side entities up to your Silverlight client. In doing this, we wanted to preserve your DataAnnotations attributes on the client, which of course meant that we needed a Silverlight version of this assembly.
(See Also: Sharing Source with Silverlight).
As our team was creating this Silverlight assembly, we found that we needed a utility to easily validate an object and/or its properties. This utility would be used by the Silverlight DataGrid and DataForm controls when objects are edited or added. We also hit some scenarios that led to new requirements for the validation attributes. The result was some fairly significant additions and changes that ended up being included in the Silverlight 3 SDK. Here are some of the crucial differences between the .NET 3.5 SP1 version of DataAnnotations, and the Silverlight 3 version:
- Validator is a static class providing utility methods for validating properties and objects.
- bool ValidationAttribute.IsValid(object) is internal in Silverlight, but public and virtual in .NET 3.5 SP1 (Desktop framework).
- ValidationResult ValidationAttribute.IsValid(object, ValidationContext) was added to replace IsValid(object) as the method attributes will override.
- ValidationAttribute.GetValidationResult serves as the public entry point instead of IsValid(object).
- ValidationResult represents an error produced as a result of validation (note that the static ValidationResult.Success is used to represent successful validation).
- ValidationContext represents the context under which validation is being performed, providing access to the object instance, its type, and other state/context.
In .NET 3.5 SP1, the only way to ask a validation attribute if it was valid for a given value was through the IsValid(object) API. When performing validation for a property attribute, the value of the property is supplied as the value to validate, giving no contextual information about the rest of the object’s properties. This severely limits what can be done with the validation attributes. With .NET RIA Services, we wanted to ensure that more context is always provided to the attributes, so we created the ValidationContext class to represent provide this information. ValidationContext has some useful properties:
- ObjectInstance is the object being validated, so that property-level validators can access the containing object.
- ObjectType is the type of the object being validated.
- DisplayName is the display name of the property or object being validated, respecting the [Display] attribute if utilized.
- MemberName is the property name or type name of the property or object being validated.
- Items is a property bag that can be used to provide additional state/context for the validation.
ValidationContext also implements IServiceProvider, so that services (such as a repository) can be provided to the attributes being validated.
It’s important to note that for cross-field validation, relying on the ObjectInstance comes with a caveat. It’s possible that the end user has entered a value for a property that could not be set—for instance specifying “ABC” for a numeric field. In cases like that, asking the instance for that numeric property will of course not give you the “ABC” value that the user has entered, thus the object’s other properties are in an indeterminate state. But even so, we’ve found that it’s extremely valuable to provide this object instance to the validation attributes.
The new Validator class relies heavily on this ValidationContext when calling its validation methods. These methods are meant to be the interface that frameworks and applications use to perform validation, rather than forcing everyone to iterate over the attributes and validate them. Validator actually comes with some low-level building blocks as well as higher-level methods. For each of the following, the Try method is a bool that indicates whether or not validation was successful, with a collection of ValidationResults being populated along the way. The method without the Try prefix is a void, throwing a ValidationException upon the first validation failure (or completing without exception when successful).
- [Try]ValidateValue takes any value and a list of validation attributes, and validates each attribute using the value and the supplied ValidationContext.
- [Try]ValidateProperty takes a value and a ValidationContext that specifies the ObjectInstance and MemberName to validate.
- [Try]ValidateObject takes an object instance and a ValidationContext where the ObjectInstance matches the instance provided, and validates all property-level and object-level validation attributes.
You’ll notice that each of these methods requires that you provide a ValidationContext, and this often trips people up because constructing a ValidationContext requires a few parameters.
Here are the constructor parameters for ValidationContext:
- object instance – The object instance being validated – such as your Customer entity. Note that this is not the property value when you are validating a property.
- IServiceProvider serviceProvider – This is an optional source to use for the ValidationContext IServiceProvider implementation. If you have services that you need to expose to your validation attributes, you can provide a service provider to the validation context, and then the attributes can retrieve the services through the ValidationContext’s implementation of IServiceProvider. Essentially, ValidationContext just serves as a wrapper around your provider.
- IDictionary<object, object> items – This is just a state bag that you can optionally provide to give your validation attributes any other state information that you so desire. The dictionary is available through the Items property on ValidationContext. Note that the dictionary is immutable during validation, so your validation attributes cannot change the dictionary values such that they would be available to subsequent validation attributes, as validation order is indeterminate.
If you don’t have a service provider or state bag that you are using, you can just use null for both of those parameters. We explicitly did not provide an overloaded constructor to omit these parameters, because we want to encourage the use of the parameters. We expect that application developers will create static CreateValidationContext methods, simplifying the construction of the ValidationContext. (In fact, that’s what RIA Services does).
The great thing about all of this plumbing that we’ve done is that it actually makes your job for validating properties and objects very straight-forward. Once you create a validation context and call into the Validator, here’s what happens:
You’ll notice that GetValidationResult is public but not virtual. That’s because it does work that wraps around the IsValid method to ensure that there’s an error message produced from invalid validation results. So when you’re creating a ValidationAttribute class, you will override the IsValid method, giving you access to the ValidationContext every time your attribute is validated.
Now, there’s one major topic missing from this longwinded story. All of this new stuff was created and released with Silverlight 3, but what about the desktop framework? The good news is that all of this is provided for the desktop framework in 3 different ways:
- Visual Studio/.NET 4.0 Beta 1
- ASP.NET Dynamic Data Preview 4
Found as version 220.127.116.11 of System.ComponentModel.DataAnnotations.dll in the Common Files folders
- .NET RIA Services July 2009 Preview
Also found as version 18.104.22.168 of System.ComponentModel.DataAnnotations.dll
Between the two frameworks, there are still some casual differences, where we couldn’t make breaking changes to the desktop framework, but we wanted Silverlight to adopt the newer API more aggressively. But regardless of which framework you’re working against, the flow will be the same. This ensures that you can create your validation attribute classes as shared code that compiles against both the desktop framework and Silverlight.
There was also one very noteworthy addition made only to the desktop version of DataAnnotations. We introduced an interface for IValidatableObject. This interface has a single method for Validate that accepts the ValidationContext (with the ObjectInstance set) and returns an IEnumerable<ValidationResult>. This method allows you to have very dynamic validation on your entities. This approach could also be useful for entities have lots of cross-field validation that cannot easily be represented with attributes.
When validating an object, the following process is applied in Validator.ValidateObject:
- Validate property-level attributes
- If any validators are invalid, abort validation returning the failure(s)
- Validate the object-level attributes
- If any validators are invalid, abort validation returning the failure(s)
- If on the desktop framework and the object implements IValidatableObject, then call its Validate method and return any failure(s)
If you are developing a framework or application that needs to validate business objects and their properties, using the Validator class is strongly recommended, as it does a lot of the grunt work for you. If you have any questions or problems regarding how you can best implement your validation, please drop me a line.