ADO.NET Data Services is a rapidly evolving set of tools that provides data access to remote clients through a set of REST-based services. The Data Services Client Library for .NET performs the magic of translating your Linq queries to URLs and passing them to the data service back-end, as well as retrieving results and hydrating objects in the client to represent them.
After running into a number of problems with the current CTP of RIA Services (see my article), I decided to fall back on Data Services to provide data access in my newest project. Data Services has the advantage of allowing you to write fairly normal Linq queries against Entity Framework entity sets, and entity data models can reside in a dedicated data model assembly (instead of requiring them to be part of the web project).
One of the differences that remain when using Data Services in Silverlight—as opposed to accessing an Entity Framework ObjectContext directly—is that Silverlight doesn’t allow asynchronous calls. So code like this, which would force a synchronous call (with FirstOrDefault), will fail in Silverlight:
var result = (from p in context.Properties where p.Required select p).FirstOrDefault();
This forces us to adopt some new patterns for data access. This isn’t a bad thing, however. And it’s an inevitable transition we’re making to asynchronous, concurrent program logic.
Here’s a typical example of querying data with Data Services in Silverlight:
var RequiredProperties = from p in context.Properties where p.Required select p; var dsq = RequiredProperties as DataServiceQuery<Node>; dsq.BeginExecute(ar => { var result = dsq.EndExecute(ar); // do something with the the result }, null);
When using a lambda statement for brevity, the syntax isn’t too bad, but the pattern gets a little more involved when you include error handling logic. If EndExecute fails, you’ll need the ability to perform some compensating action.
So what I’ve done to keep my client code simple is to define an extension method called AssignAsync that encapsulates this whole pattern.
public static class DataServicesExtensions { public static void AssignAsync<T>(this IEnumerable<T> expression, Action<IEnumerable<T>> Assignment, Action<Exception> Fail) { var dsq = expression as DataServiceQuery<T>; dsq.BeginExecute(ar => { IEnumerable<T> result = null; try { result = dsq.EndExecute(ar) as IEnumerable<T>; } catch (Exception ex) { Fail(ex); return; } Assignment(result); }, null); } }
This enables me to write the following code:
var RequiredProperties = from p in context.Properties where p.Required select p;
RequiredProperties.AssignAsync(result => properties = result,
ex => Debug.WriteLine(ex));
In other words: if the query succeeds, assign the result to the properties collection; if it fails, send the exception object to Debug output. Either action can be used to send signals to other parts of your application that will respond appropriately. Instead of Debug.WriteLine, you might add the exception object to some collection that triggers an error dialog to appear and your logging framework to record the event. Instead of assigning the result to a simple collection, you could convert it to an ObservableCollection and assign it to an ItemsControl in WPF or Silverlight. Anything is possible.
As I explore Data Services further, I will be looking for ways to share query and other model-centric logic between Silverlight and non-Silverlight clients. I suspect that the same asynchronous patterns can be used in non-Silverlight projects as well, and that those projects will benefit from this query style.