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
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.
Comments
- Anonymous
January 19, 2010
I have one question about the CurrentDataSource property: public object CurrentDataSource { get { return _currentDataSource;} set { _currentDataSource = value as T; } }The set method use the "as" operator but would not a cast be more appropriate? This way if perhaps someone where to pass a value to the property that was not of the type 'T' they would get an exception on that set operation rather than the NullPointerException later on. - Anonymous
January 20, 2010
@James PYeah you're right. Or at the very least I should have my own null check.Thanks for the spot.Alex - Anonymous
June 24, 2010
Chinese versionwww.cnblogs.com/.../DSP6.html - Anonymous
May 21, 2012
I have a question as I'm confused about the appropriate way to go about creating a Custom DataService Provider when I need to pull Data from a Web Service which I do not own or manage. It has no IQueryable support either for retreiving Data from a Url (i.e. No OData syntax). It's just a traditional rigid Web API. Returns Collections of Data and requires Parameters to be passed in depending on what Method you are calling.Now, based on the samples, they all use "In-Memory" Lists, which already leverage a Query Provider (LINQ to Objects). I would like a good sample or a Blog which shows how to create a DataService Provider and/or Query Provider for a REST API (i.e. Twitter, SharePoint, or old traditional ASMX Web Service). I'm building a new Web Application (MVC 4) and it displays a lot of Charts/Graphics and some Grids with Paginiation. I'll be using JavaScript to query the Data from the Server (using REST APIs). I would be all set to go using the MongOData Provider / LINQ to MongoDB stuff, but unfortunately, I'm not in charge of building the Web Service which retrieve Data from the MongoDB. This is being developed by another Team and they are building "traditional" Web APIs (MVC 3) and won't be supporting "IQueryable" and what not.I've tried to convince them to use OData or Web API (MVC 4), but no luck. I really want to structure my Data using Models (with Navigation Properties), so it will be easy to query the Data and build the UI Charting.So my game plan is build my own OData API (using WCF Data Services v5) or Web API (MVC 4) for my Web Interface. Either supports IQueryable for the "Get" Queries over Collections. I'm leaning towards OData/WCF Data Services because of the stronger Query support, Navigation Properties, Pagination continuations, Change Tracking, etc. I simply want to create a POCO Entity Data Model, and then "capture" the IQueryable Expressions being used to query the Data and then turn them into the appropriate Web Service calls to the underlying Data Source. Keep in mind I'll need to parse the Expression Tree and get Values so I can pass those in a Parameters to the Web Service Call (i.e. $take, $skip). I DO NOT want to pull all the Data in from the Web Service first, and then apply expressions. There is massive amounts of Data.I believe this really easy to do using MVC WebAPI as I have full control over the Get Method. I can also just take Parameters from my Web API Method and align them he underlying Web Service Method, but then I'll loose the ability leverage the "$take, $skip, $filter, and $orderby operators. I suppose I could manually parse this, but then I'm back to just creating an Query Provider at that point, and I'm back to OData.My belief based on the Blog is that this possible using Custom Query Provider and/or Data Service Provider. I'm just not clear on exactly where to parse the Expression Tree and make the Web Service Call.Any help would be appreciated.Thanks