WCF Data Services and the IExpandProvider
Yesterday, a customer ran into a weird issue with our OData/WCF Data Services support in LLBLGen Pro: when an $expand directive was given which was more than one level deep, the service would only return the first level. $expand is OData's directive to eager load additional data into the data requested. So if I for example want to read the data of customer 'ALFKI' from the Northwind database and also that customer's orders, I'd issue this OData query:
This will return the data of ALFKI and the data of all Orders of that customer. This all worked fine. However, when I also would specify to load all OrderDetail entities for each order (so i.o.w.: a second level expand), it would not return these OrderDetail rows, but simply return the same data as the query above:
This could be caused by a lot of things of course, but as it has worked in the past and it didn't work today, what did change? First I verified with ORM Profiler that the engine indeed executed three queries: one for Customer with a filter on CustomerID, one on Orders with the same predicate and one on OrderDetails with a filter on all OrderIds returned by the Orders query, a typical three node prefetch path query. Debugging the engine revealed that the expand directives were properly converted to a prefetch path, and the data was properly merged into a single graph of Customer – Orders – OrderDetails.
This frustrated me to no end, because my code did what it was told to do, it handed over the proper entity instance graph to the calling service code and apparently something went wrong there, but what and why was a mystery. Not only that, but who to ask what's wrong and if something was wrong on Microsoft's end, would they ever fix it? Writing the WCF Data Services support code for our O/R mapper framework was a frustrating experience as there's little documentation available how to add WCF Data Services support for your ORM/Data access technology (as everything about WCF Data Services is tightly designed around Entity Framework) so you have to piece together what you have to do from blog-posts, simple examples and decompiling (sorry) the Entity Framework WCF Data Services provider.
First rule of fixing issues when writing code to support a Microsoft framework is to look whether you're the only one having this problem. I was. Well, I did find a question on stackoverflow which asked what to do about the fact that IExpandProvider interface was deprecated. This refreshed my memory on it a bit. IExpandProvider is the interface which is to be implemented by the class which handles $expand fragments. That is, it was. Microsoft deprecated this interface some time ago but if your WCF Data Services service implemented the interface, it would still work as if nothing happened. For LLBLGen Pro v4 we moved our WCF Data Services code to the nuget version of the WCF Data Services code instead of the one shipped with .NET 4/4.5. Everything worked properly, so we signed it off. But we didn't have a test with two-level expands, so we missed an important aspect: it apparently seems the case the v5.x version of WCF Data Services doesn't return multiple levels of data fetched with $expand, if the service provides an IExpandProvider implementing object.
Reading the reply on the stackoverflow question by Vitek Karas, one of the WCF Data Services team members, I ended up on a blogpost which went on and on about expand wrappers, projection wrappers and what not, as an alternative to IExpandProvider implementing classes. It was unclear to me if I had to do that all myself or not: this is typical for what I had to wade through when writing the original WCF Data Services support code: there's no real documentation for this. As it looked rather daunting, and I have the feeling the guy who posted the question on stackoverflow thought the same, I had enough: I just want to provide top-quality code to my customers, but this endless stream of frustration made it impossible for me to do so.
I can understand why Microsoft has less eye for us, 3rd party ORM developers: we're a small group, we compete with their ORM framework which is there to keep people on .NET/MS platforms, Microsoft is still a product-oriented company and so other things might have higher priority. That doesn't make it less frustrating though.
After a good night sleep I woke up this morning with a daft thought in my mind: what if I simply told the service there was no IExpandProvider implemented? What would the service do? So I changed my WCF Data Services support code to simply return null if it was asked to return an IExpandProvider implementation and re-ran the tests. They worked! All levels of data were returned, and the queries were still efficiently done. But how? After all, there was no prefetch path handler code active anymore, and the prefetch path API is not the same as Entity Framework's (ours can do filters on included data, sort, exclude fields etc.).
If there's no IExpandProvider, the WCF Data Services creates a custom projection with the expand fragments as nested queries. I described them here more in detail. Our Linq provider recognizes these nested queries and performs a similar procedure as if it would do with prefetch paths: each nested query is executed as a separate query and the results are merged using hash maps in-memory with parent nodes and nested queries are filtered based on the data fetched in parent nodes. This means the engine will still run three queries on the Customer – Orders – OrderDetails expand path, though it will use the nested query pipeline in the linq provider (which can deal with nested custom projections as well as nested entity sets).
The fix was simple for my code: comment out some lines and everything was fine. But as I commented on Vitek's stackoverflow reply: what if the ORM has a proper eager-loading pipeline? It now has to rely on nested queries, something which isn't done properly in e.g. Linq to Sql (they're loaded in a SELECT N+1 fashion), unless a lot of work is done to make this load properly. You can't simply mark a working system 'obsolete', and expect frameworks supporting your service to say 'ok, no problem, we'll invest again some time and write some more code'.
There are two scenario's for fetching additional data: fetch additional entities with the entity loaded, or fetch a custom projection of a set of nested sets, because if you fetch an entity E, where in E do you store the custom projection of some related data? Why not keep IExpandProvider around for the scenario where you fetch entities and related entities, the normal eager load scenario? If one needs $expand to fetch a custom projection with nested sets, why not use the mechanism that's available now to replace IExpandProvider's logic? Just to support custom selects on entities to exclude fields for Entity Framework? Isn't it then better to implement an exclude/include facility in EF for that?
Anyway, it's solved now and I'm glad this scenario has a happy end as I didn't expect it would. I hope it helps some poor souls out there who are faced with the same problem and I also hope some people within Microsoft do realize that if you make it difficult for 3rd party frameworks to support your frameworks, they might stop doing so.