Earlier today, my SilverlightTV recording on RIA Services and Validation went online. I used validation as a feature area to focus on this first recording on RIA Services, because I think it illustrates both the RIA Services value proposition and key elements of the vision around the project in a very direct manner. Specifically:
- Focus on end-to-end solutions for data scenarios. It is not sufficient to just address querying data or submitting some changes, but about providing the infrastructure for managing data, editing with validation, tracking errors raised on client and/or server, rolling back changes, and more.
- The server project and client project are logical halves of the same application. It would be great to preserve and propagate developer intent, and semantics from the database to the middle tier to the client, and share code across tiers where possible. In other words leverage intent and rules as close to the end-user, while enforcing them at each tier. Automatically.
- Provide out-of-box solutions to common scenarios. The Entity class on the client provides a pretty complete implementation of working with data on the client. It provides identity management, change tracking, change notification, transacted editing and rollback capabilities, as well as tracking and surfacing validation and concurrency conflict errors. In particular, in the context of this post, Entity provides an implementation of infrastructure interfaces such as INotifyDataErrorInfo introduced in Silverlight 4, so you don't have to roll your own.
In my demo for SilverlightTV, I used a product catalog and product editing scenario (my favorite scenario if you ask folks on the team). However for MIX10, I used a Book Club scenario, which I think is quite a bit more interesting (with the potential to become my new favorite scenario).
Check out the video. And then come back to read further. My MIX10 summary post lists everything else going on in the Book Club sample (along with a link to all of the sample code), but the rest of the post focuses on the validation aspects.
Update 3/26/10: For those looking for a copy of the app demo'd on the SilverlightTV show, you can download the Store sample.
Validation Overview/Background
RIA Services supports multiple levels of validation - member/field level validation, entity level validation, and operation level validation. Validation can be expressed as declarative rules via metadata annotations, or implemented via imperative rules in code attached through metadata annotations. These are the metadata annotations in System.ComponentModel.DataAnnotations (ValidationAttribute, and its derivatives). While there is a strong reliance on metadata, a less known fact is that attributes are simply the runtime representation of validation rules - by default, the source of this metadata is based on reflection over types and based on the IL in an assembly, but in reality, you can write custom metadata providers that can pull metadata from any source (such as an XML file, a database, a rules engine or whatever). Metadata attributes simply provide a standardized representation for consumers of the validation semantics.
Validation can happen on the client in addition to the server. It can happen as part of processing a change set submitted to the server or asynchronously as the user is editing an entity in the UI on the client.
As such you can see we have quite deep and extensive support for validation as a core scenario in RIA Services. So now let's look at the different aspects of the validation feature that surface in the Book Club application.
Database Constraints to Validation - Automatically Inferred Validation Rules
A database schema typically defines some basic constraints. For example, a column cannot be null. Or the maximum size of a column. These semantics are captured into the data model if you are using data access technologies such as LINQ to SQL or Entity Framework (as well as other ORMs).
RIA Services provides the smarts to translate this ORM-specific metadata into DAL-agnostic metadata defined in System.ComponentModel.DataAnnotations (eg. RequiredAttribute and StringLengthAttribute), and also propagates it to the client as part of the code generation from server to client. This allows the infrastructure on the client to lookup and apply these rules without the developer having to do anything explicitly. To see this concretely, simply open up the generated code, and check out the metadata on entity members.
The concept at play here is there is a metadata pipeline that spans from database to client, and developer intent flows downstream, without having to re-specify on each tier.
In the Book Club application, the Title column of the Book table is marked as non-nullable. This is reflected on the client with a [Required] attribute on the Title property of the Book entity, causing the following error to show up in the UI when I edit a Book but leave Title as blank.

Declarative Validation
The developer can annotate the entities on the middle tier via attributes to add additional constraints. For example in the sample, the Book entity has an ASIN property that represents the Amazon ID of a book. This needs to be an alphanumeric field that is 10 characters long. One way to represent this is through a regular expression. There is a [RegularExpression] attribute I can apply.
So here is the code I add on the server:
[MetadataType(typeof(BookMetadata))]
public partial class Book {
private static class BookMetadata {
[RegularExpression("^[A-Za-z0-9]{10}$",
ErrorMessage = "The ASIN must consist of uppercase letters and digits and be 10 characters long.")]
public static readonly object ASIN = null;
}
}
Essentially what I am doing is adding a metadata type, BookMetadata, that attaches additional bits of metadata to the Book type.
RIA Services detects this metadata annotation, and determines that there is an implementation of RegularExpressionAttribute on the Silverlight client as well, and as a result propagates this metadata attribute. This allows the client to run the regular expression and validate the ASIN property while a Book is being modified in the user interface. Here is the error I see:

You can extend this system, by creating your own custom validation attributes derived from ValidationAttribute, and making it available on both the server and client if appropriate.
Custom Validation
Typically a framework developer will create custom derived ValidationAttribute classes to extend the validation system. An application developer however can choose a simpler route and simply write a class with a validation rule method, and attach that to the entity using the built-in [CustomValidation] attribute.
Here is an example:
[MetadataType(typeof(BookMetadata))]
[CustomValidation(typeof(BookRules), "AddedDateFollowsPublishDate")]
public partial class Book {
private static class BookMetadata {
[CustomValidation(typeof(BookRules), "DescriptionIsMeaningful")]
public static readonly object Description = null;
}
}
Here I've added both an entity-level and a field-level custom validation rule. Lets look at the implementation of these.
public static partial class BookRules {
public static ValidationResult DescriptionIsMeaningful(string description, ValidationContext context) {
if (String.IsNullOrEmpty(description)) {
return ValidationResult.Success;
}
string[] words = description.Split(new char[] { ' ', '\t', '\n', '\r', },
StringSplitOptions.RemoveEmptyEntries);
if (words.Length > 5) {
return ValidationResult.Success;
}
return new ValidationResult("The description must be at least a sentence to be meaningful.",
new string[] { "Description" });
}
public static ValidationResult AddedDateFollowsPublishDate(Book book, ValidationContext context) {
if (book.PublishDate > book.AddedDate) {
return new ValidationResult("The book must already be published.",
new string[] { "PublishDate" });
}
return ValidationResult.Success;
}
}
These rules contain your implementation and you can perform any logic you need to perform. Here is what happens when the Description property of a Book isn't fully specified:

Since these validation rules don't access any server-specific resources or data, they could be executed on the client as well. If this class is defined in a source code file such as BookRules.shared.cs, that causes the file to be linked in into the client project as well.
Server Only and Operation-Level Validation
So far the validation I've been showing was based on rules defined on the entity and enforced on both client and server. Some validation rules apply to operations, for example, only when you are inserting a Book. So they are tied to the operation rather than the entity. Additionally, some rules might need to do a database lookup, or access other server-side resources and as such cannot be executed on the client.
I can attach a validation attribute on the insert method on the DomainService, or implement the rule imperatively within the DomainService method (which is what I do in the sample).
Specifically, in the example, I want to ensure that the ASIN being specified for a book is a valid one, by doing a lookup against the Amazon product catalog (which requires a web service call to Amazon APIs using a secret key stored in server-side configuration that is not sent down to the client):
public class BookShelfService : LinqToEntitiesDomainService<BookClubEntities> {
[Insert]
public void ShareBook(Book book) {
...
AmazonService amazonService = new AmazonService();
if (amazonService.IsValidASIN(book.ASIN) == false) {
ValidationResult error =
new ValidationResult("The specified Amazon ID did not resolve to an actual book.",
new string[] { "ASIN" });
throw new ValidationException(error, null, book);
}
...
}
}
When the user makes some edits on the client, for example, editing a book, and inserting a new book, the user interface displays the pending changes via edit markers (yellow for modified, and green for new) as such:

When the user clicks the Save button all the pending changes are packaged into a change set and submitted to the server. As the DomainService methods are invoked on the server, any validation errors are collected and sent back to the client. A good client experience will inform the user there were errors and highlight the entities in error state. In the sample, the UI changes to show error markers, and when the entity is selected, the edit form shows up with error indicators and the error message as raised by the server as shown in this screenshot:

Asynchronous Validation
Sometimes it would be nice to inform the user in advance of an error that can only be determined on the server. For example, if the book being inserted already exists in the database, it would be nice if that could be surfaced in the edit form while the user is editing the book details, rather than committing to the list of pending changes, and only later seeing the error.
In this case, the client user interface doesn't have access to all the books on the client, and so this validation itself needs to be implemented as a server-side validation scenario. However the functionality of the rule can be exposed as an invokable service method to the client (a method can be invoked outside the context of a change set being submitted).
Here is what I have in my service. The BookExists method is used on the server, but also exposed to the client as a regular service method that can be invoked asynchronously.
public class BookShelfService : LinqToEntitiesDomainService<BookClubEntities> {
[Insert]
public void ShareBook(Book book) {
if (BookExists(book.ASIN)) {
throw new ValidationException("The book already exists and cannot be added.");
}
...
}
public bool BookExists(string asin) {
bool bookExists = false;
using (BookClubEntities db = new BookClubEntities()) {
bookExists = db.Books.Any(b => b.ASIN == asin);
}
return bookExists;
}
}
On the client, the application invokes this same BookExists method, and if the server returns true, the client-side code adds a validation error to the list of validation errors being tracked on the Book entity. The entity uses the INotifyDataErrorInfo infrastructure to inform the UI of a new error. The UI picks up this notification to show an error as show in the screenshot below:

Here is the client code to invoke the service method and add an error:
public void CommitEditing() {
if (IsEditing == false) {
return;
}
if (SelectedBook.Validate(_bookShelfContext.ValidationContext) == false) {
return;
}
if (SelectedBook.IsNew) {
_bookShelfContext.BookExists(SelectedBook.ASIN,
delegate(InvokeOperation<bool> operation) {
if (operation.Value) {
ValidationResult duplicateError =
new ValidationResult("This book already exists in the book club.",
new string[] { "Title" });
SelectedBook.ValidationErrors.Add(duplicateError);
}
else {
EndEditing();
}
}, null);
}
else {
EndEditing();
}
}
Summary
So there you go - a sample that demonstrates automatic validation, declarative validation, member and entity level custom validation, server-only validation and asynchronous validation. RIA Services provides the capability for you to incorporate validation to create functional end-user experiences in data entry scenarios.