Wednesday, January 12, 2011

OData validation using DataAnnotations

WCF DataServices (OData) becomes more popular every day. If you need to expose data between different applications or even between different tiers of the same application, it is definitely a really good option.

Unfortunately, one of the missing features of the current version (v4.0) is the validation using DataAnnotations (if you want that feature to be implemented in the next version, just vote for it here). In this post I am going to show how to implement this validation using a ChangeInterceptor. I hope it helps you.

Update [24 Feb 2011]: OData team is working in a better solution to this scenario, for more details click here.



Here we have a Customer object (POCO) that is mapped in the Entity Framework model:

public class Customer
{
    public int Id { get; set; }

    [Required]
    public string Name { get; set; }
}

As you can see, the Name property is decorated with the Required attribute (DataAnnotations).

In the following code snippet we can see the WCF DataService that is exposing the Customers EntitySet:

public class WcfDataService : DataService<DatabaseEntities>
{
    public static void InitializeService(DataServiceConfiguration config)
    {
        config.SetEntitySetAccessRule("Customers", EntitySetRights.All);
        config.DataServiceBehavior.MaxProtocolVersion =
                                          DataServiceProtocolVersion.V2;
    }
}

So far nothing new. The first thing we need to do in order to add the validation logic is to create a ChangeInterceptor method:

public class WcfDataService : DataService<DatabaseEntities>
{
    ...

    [ChangeInterceptor("Customers")]
    public void ValidateCustomers(Customer customer, UpdateOperations operation)
    {
       // Validation logic
    }
}

After that, we just need to add the following validation logic:

[ChangeInterceptor("Customers")]
public void ValidateCustomers(Customer customer, UpdateOperations operation)
{
    // Only validates on inserts and updates
    if (operation != UpdateOperations.Add && 
        operation != UpdateOperations.Change)
        return;

    // Validation
    var validationContext = new ValidationContext(customer, null, null);
    var result = new List();
    Validator.TryValidateObject(customer, validationContext, result, true);

    if(result.Any())
        throw new DataServiceException(
            result
            .Select(r => r.ErrorMessage)
            .Aggregate((m1, m2) => String.Concat(m1, Environment.NewLine, m2)));
}

As you can see I am using the Validator class (System.ComponentModel.DataAnnotations namespace) and I am throwing a DataServiceException in case the validator finds any error.

Edit: The last parameter of the TryValidateObject method (validateAllProperties) should be set to true, otherwise it will only validate the [Required] attribute. More about this issue here.

If you are using the MetadataType attribute instead of having the DataAnnotation attributes in your POCO, you need to add some additional lines of code in order to support that. Check the attached source code to see how to do it.

Finally, in the client we are going to receive a DataServiceRequestException that will contain the error we sent from the server.

try
{
    var serviceUri = new Uri("http://localhost:4799/WcfDataService.svc")
    
    var context = new DatabaseEntities(serviceUri);

    var customer = new Customer();

    context.AddToCustomers(customer);

    Console.WriteLine("Calling data service...");

    context.SaveChanges();

    Console.WriteLine("Insert successful");

}
catch (DataServiceRequestException ex)
{
    if(ex.InnerException != null && ex.InnerException.Message != null)
        Console.WriteLine(ex.InnerException.Message);
    else
        Console.WriteLine(ex.ToString());
}



As you can see, our original error message is now wrapped inside an xml document (the serialized original exception). If you need to get the original message from there, you can either read the xml or use the code shown by Phani Raj in this post.

To download the source code, just click here.

1 comment:

ramki005 said...

Your idea is okay.but if u going delete that time what will be happen ?