Creating a Data Service Provider – Part 5 - Query

In Part 4 of our series showing how to implement a Custom Data Service Provider we hooked up an incomplete implementation of IDataServiceQueryProvider, just enough to get the ServiceDocument and $metadata working.

In this part we’ll get query working too.

To do that we need to know where the data is going to come from – the DataSource – and we need to really implement IDataServiceQueryProvider.

Creating a DataSource:

So where is our Product data going to live?

How about a Context, a little like EF or L2S but in memory. First we need a base class.

public abstract class DSPContext
{
public abstract IQueryable GetQueryable(ResourceSet set);
}

Next we need a strongly typed context to expose a Products resourceSet:

public class ProductsContext: DSPContext
{
private List<Product> _products = new List<Product>();
public override IQueryable GetQueryable(ResourceSet set)
{
if (set.Name == "Products") return Products.AsQueryable();
throw new NotSupportedException(
            string.Format("{0} not found", set.Name)
);
}
public List<Product> Products
{
get {
return _products;
}
}
}

Nothing too tricky here at all. As you can see the ResourceSet is backed by a simple List.

The only thing worth noting is that, because our data is in memory, we use .AsQueryable() to get the LINQ to Objects implementation of IQueryable.

Next we need to change the Sample service to use the ProductsContext as its DataSource:

public class Sample : DSPDataService<ProductsContext>
{

Then we override CreateDataSource in our Sample service, so that we can pre-populate the Products list with some data:

protected override ProductsContext CreateDataSource()
{
ProductsContext context = new ProductsContext();
context.Products.Add(
new Product {
ProdKey = 1,
Name = "Bovril",
Cost = 4.35M,
Price = 6.49M
});
context.Products.Add(
new Product {
ProdKey = 2,
Name = "Marmite",
Cost = 4.97M,
Price = 7.21M
});
return context;
}

Finally we override the GetQueryProvider method we added last time to look like this:

public override IDataServiceQueryProvider GetQueryProvider(
IDataServiceMetadataProvider metadata)
{
return new DSPQueryProvider<ProductsContext>(metadata);
}

IDataServiceQueryProvider for strongly typed data:

Lets throw away our ‘running service only’ implementation from Part 4.

It didn’t do anything useful anyway.

What we need is a strongly typed in memory Query Provider:

By ‘strongly typed’ I mean for every ResourceType and its Properties there needs to be a matching CLR type with matching properties.

There isn’t that much code, so here it is:

public class DSPQueryProvider<T> : IDataServiceQueryProvider where T : DSPContext
{
T _currentDataSource;
IDataServiceMetadataProvider _metadata;

    public DSPQueryProvider(IDataServiceMetadataProvider metadata)
{
_metadata = metadata;
}
public object CurrentDataSource
{
get { return _currentDataSource;}
set { _currentDataSource = value as T; }
}
public IQueryable GetQueryRootForResourceSet(
ResourceSet resourceSet)
{
return _currentDataSource.GetQueryable(resourceSet);
}
public ResourceType GetResourceType(object target)
{
Type type = target.GetType();
return _metadata.Types
.Single(t => t.InstanceType == type);
}
public bool IsNullPropagationRequired
{
get {return true; }
}
public object GetOpenPropertyValue(
object target,
string propertyName)
{
throw new NotImplementedException();
}
public IEnumerable<KeyValuePair<string, object>> GetOpenPropertyValues(object target)
{
throw new NotImplementedException();
}
public object GetPropertyValue(
object target,
ResourceProperty resourceProperty)
{
throw new NotImplementedException();
}
public object InvokeServiceOperation(
ServiceOperation serviceOperation,
object[] parameters)
{
throw new NotImplementedException();
}
}

What we’ve implemented:

CurrentDataSource holds a reference to the current ProductsContext in _currentDataSource.

Next in GetQueryRootForResourceSet() we delegate to _currentDataSource.GetQueryable() to find the IQueryable for the specified ResourceSet.

We implement GetResourceType(object) , by getting the CLR type of the current object and looking in our IDataServiceMetadataProvider.Types to find the the one with a matching InstanceType.

Finally we return true from IsNullPropagationRequired, because we need Data Services to compensate for the lack of NullPropagation in LINQ to Objects.

NullPropagation?

If you take a LINQ query like this:

var results = from x in queryRoot
select x.Customer.Name;

This could easily throw a NullReferenceException, if x.Customer is ever null.

To avoid this Data Services can inject null checks (aka inject NullPropagation logic) into the query. All you need to do is return true from IsNullPropagationRequired.

NOTE: some IQueryables like L2S and L2E send queries to database which seamlessly support null propagation. Hence the option.

What we didn’t implement:

Our Data Service Provider is strongly typed, so Data Services will never call GetPropertyValue, because it knows how to get a property values directly from the resource object.

The model we expose from our Data Service doesn’t have any ServiceOperations or OpenTypes with OpenProperties so we can safely throw NotImplemented Exception from all of those methods.

Conclusion:

Finally we have a complete Read/Only data service up an running

ROTypedService 

We’ve got a long way to go though. We still need to add support for Update, Relationships, Service Operations, Streaming, Paging and Open Types and I want to show you how to do this all without backing CLR types too.

In Part 6 we’ll talk a little about the interaction between Data Services and your Customer Data Service Provider during a typical GET request.