Throughout this series on RIA Services Validation, I’ve mentioned and shown ValidationContext several times. In this post, we’ll learn about the purpose of ValidationContext, what types of context it can provide, and how it integrates with RIA Services. This will allow us to explore further topics such as cross-entity validation and open the door for other advanced validation scenarios.
ValidationContext Purpose
Although ValidationContext exists within the System.ComponentModel.DataAnnotations assembly in both .NET 4.0+ and Silverlight 3+, the class was introduced by the RIA Services team. While designing the RIA Services validation system, we considered scenarios where validation logic is based on factors outside the validation method itself. This can range from the user’s settings to UI state to other records within the system. In addition to the object and property a validator is being executed against, validators much be able to reach back into the system to extract whatever data is needed.
Every time RIA Services invokes validation, a ValidationContext is provided to every validator. With this, validators can access state and services two ways: 1) With an Items property bag that can store any state necessary, and 2) By retrieving services made available by the application.
Items Dictionary
ValidationContext offers a Dictionary<object, object> Items property that can be used as an arbitrary property bag. Any state needed by your validators can be stored and accessed here. Examples might be the current user of the system (so that a user’s role can affect validation logic), user preference settings, or anything else specific to how the user is interacting with the system. Think about all of the places where your views track some type of state that drives validation logic – that state can be stored in ValidationContext.Items so that your validators can access it easily.
IServiceProvider and IServiceContainer
Consider the scenario where a validator needs to access the database; you certainly wouldn’t couple a validation method to your data access layer, would you? Instead, you would make your repository available as a service that your validation method can consume in a loosely-coupled manner. In order to easily support these scenarios, ValidationContext implements IServiceProvider. This interface requires a single method, GetService(Type). Using GetService, a validation method can access your repository without knowing how to instantiate or initialize it.
Although Silverlight doesn’t provide an IServiceContainer interface, the .NET framework does. On the server, ValidationContext utilizes this interface and exposes a ServiceContainer property that can be used for registering services. Any service registered through ValidationContext.ServiceContainer will be available through ValidationContext.GetService.
ValidationContext Constructor
The ValidationContext class has a single constructor accepting the object instance to be validated, an IServiceProvider, and an Items dictionary. When you are constructing a ValidationContext, you can supply null for the service provider and/or items dictionary if you aren’t using them.
When validation is about to be invoked for a value, it’s necessary that the object instance provided to the ValidationContext constructor matches the ObjectInstance being validated. However, when constructing a ValidationContext that will be used as a seed for future validation (as we’re discussing in this post), the object provided to the constructor is unimportant and arbitrary.
If you supply an IServiceProvider, any calls to ValidationContext.GetService will utilize that provider. If you pass null for the service provider, GetService will not use any backing store – but using ValidationContext.ServiceContainer.AddService still allows you to provide services individually.
If you supply an IDictionary<object, object> for the items parameter, ValidationContext.Items will utilize this dictionary as the seed. Additional items can be added after construction, but if you have a prepared dictionary, you can specify it easily. If you pass null for the items, then an empty dictionary will be created as the seed.
With both your Services and Items, anything you provide will be used as a seed for future validation. Each invocation of validation will in fact use a new instance of ValidationContext, but your Items and Services are re-used.
RIA Services Integration
Of course, in order for validators to get any use out of ValidationContext, RIA Services needs to offer a means for putting items and services into ValidationContext. The approaches for this differ between the server (DomainService) and the client (DomainContext) because the calls are made in different ways.
DomainService (Server-Side)
Your DomainService is a near-stateless service. Every time your service is invoked on the server, a new instance of your DomainService is instantiated and executed. When the request ends, the DomainService is disposed. Your DomainService is the gateway into your back-end system, and therefore it’s expected that this class would have access to all state related to validation as well as any services that validation might rely on. To provide state and services, the DomainService class has a protected read/write property for ValidationContext.
The intent with this design is that your DomainService can construct its custom ValidationContext, providing any Items or Services, and set the ValidationContext property on the DomainService during the Initialize method. Here’s an example of a MeetingService that does this:
[EnableClientAccess()]
public class MeetingService : LinqToEntitiesDomainService<MeetingStoreEntities>, IMeetingDataProvider
{
/// <summary>
/// Initialize the <see cref="DomainService"/>, setting the
/// <see cref="ValidationContext"/> with custom state and services.
/// </summary>
/// <param name="context">
/// Represents the execution environment for the operations performed
/// by a System.ServiceModel.DomainServices.Server.DomainService.
/// </param>
public override void Initialize(DomainServiceContext context)
{
var contextItems = new Dictionary<object, object>
{
{ "AllowOverBooking", HttpContext.Current.Session["AllowOverBooking"] ?? false }
};
this.ValidationContext = new ValidationContext(this, context, contextItems);
this.ValidationContext.ServiceContainer.AddService(typeof(IMeetingDataProvider), this);
base.Initialize(context);
}
...
The Initialize method is the perfect place to provide the customized ValidationContext for a few reasons. First, this method is always called before any validation occurs. Second, the DomainServiceContext is provided, which offers context about the domain service operation being performed, allowing the ValidationContext to be altered based on that state. And third, DomainServiceContext implements IServiceProvider and can therefore be used as the seed of services to be provided to the ValidationContext—any services available to your DomainService are now also available to your validation methods.
Beyond the services already available from the DomainServiceContext, you can register additional services using the ValidationContext.ServiceContainer.AddService method. In this example, the DomainService is making itself available as an IMeetingDataProvider instance. We’ll learn more about how this is used in the next blog post.
DomainContext (Client-Side)
On the client, our validation methods are called a bit differently from the server. Whereas on the server we had a near-stateless service, the client has a very stateful DomainContext. Our DomainContext serves as our client-side cache of entities, and the proxy to invoke server-side operations. The lifetime of a DomainContext can be very long, in fact some scenarios have a DomainContext stay alive for the entire lifetime of the application. Your client application consumes and operates on your DomainContext, but your DomainContext does not control your application state or logic, and therefore it should not be in control of the ValidationContext either.
To accommodate the client-side usage scenarios for validation context, you’ll see that every DomainContext has a public read/write ValidationContext property. Emphasis on public. This means that any consumer of your DomainContext can set the ValidationContext property at any time, or add items into its Items dictionary at any time. Moreover, consumers of your DomainContext control what services are available from the ValidationContext.GetService by supplying a custom IServiceProvider argument on the ValidationContext constructor.
Your DomainContext consumers will be your Views, or more optimistically, your ViewModels. Here’s some code that creates a custom ValidationContext and sets it on a MeetingContext instance:
var contextItems = new Dictionary<object, object>
{
{ "AllowOverBooking", this.allowOverBooking.IsChecked ?? false }
};
var contextServiceProvider = new SimpleServiceProvider();
contextServiceProvider.AddService<IMeetingDataProvider>(this.meetingContext);
this.meetingContext.ValidationContext = new ValidationContext(
this.meetingContext, contextServiceProvider, contextItems);
There are two major differences on the client:
- There is no IServiceProvider instance given to you, nor is there a concrete implementation, so you’ll have to create one;
- There is no IServiceContainer interface and no ServiceContainer property on ValidationContext, so you cannot add services after creating your ValidationContext.
Here is the world’s simplest IServiceProvider implementation, and it’s what I have used several times:
public class SimpleServiceProvider : IServiceProvider
{
private Dictionary<Type, object> _services = new Dictionary<Type, object>();
public object GetService(Type serviceType)
{
if (this._services.ContainsKey(serviceType))
{
return this._services[serviceType];
}
return null;
}
public void AddService<T>(T service)
{
this._services[typeof(T)] = service;
}
}
Once the ValidationContext property has been set on your DomainContext, all validation invoked through the DomainContext or through any Entity attached to the DomainContext will use this ValidationContext as the seed.
Detached Entities, DataGrid, and DataForm (Beware of the gaps)
On the client, there are some gaps in ValidationContext’s use. The first is with detached entities—because the ValidationContext is provided to a DomainContext, any validation performed by an entity before it’s attached to a DomainContext will not have access to the custom ValidationContext you supply. This is very obvious when you instantiate a new entity and set its properties immediately; property-level validation occurring at that time will not have access to your Items or Services. As soon as you attach the entity to your DomainContext though, this connection is made.
DataGrid and DataForm are controls in the Silverlight SDK and Toolkit respectively. These controls have no dependency on RIA Services (or vice versa), even though they integrate with RIA Services very well. Because there is no dependency on RIA Services, there is no means for the controls to access your DomainContext or its ValidationContext. Therefore it’s important to understand that any validation invoked by these controls will not have access to your Items and Services. But worry not, when your entities are attached to your DomainContext, the property setters themselves invoke validation using your ValidationContext. Similarly, entities validate themselves when you finish editing them and your ValidationContext is used then too. Lastly, when you call SubmitChanges(), your entire changeset is validated again, and the ValidationContext is available then too.
What you must take away from this is that your validation methods cannot assume access to your ValidationContext Items and Services. You must author your client-side validation methods such that they succeed when any required items or services are unavailable. We’ll see concrete examples of this in the next blog post.
RIA Services Validation Recap
In this article, we’ve learned how RIA Services uses ValidationContext as a mechanism for your applications to register state and services for use by validation methods. Your DomainService can set its own ValidationContext from the Initialize method and all server-side validation will use this context. Your client-side application code can set the ValidationContext on the DomainContext instances used and offer state and services as well. On the client, not every invocation of validation will have access to the state and services though, so validation methods must be able to fall back to success.
This article is part of an in-depth series on RIA Services Validation. Here’s the full series:
[9/6/2011] The source code for everything shown during the series is available on GitHub.
Digging Deeper
In the next post, we’ll see some examples of how to leverage the ValidationContext that we just learned about. The examples will include a couple of cross-entity validation scenarios. After that, I’ll provide a validator factory that allows validation rules from entities to be inherited into a ViewModel.