Adding OData Inline Count Support to the ASP.NET Web API
Note: This post assumes that you’re working against the NuGet bits as of 4/5/2012, in which OData support is limited to $top, $skip, $orderby, and $filter. I plan to go back and check whether the latest bits already have $inlinecount support now that the ASP.NET Web Stack has been open-sourced.
Update: Moving older updates to the bottom of the post. Short version: this worked in theory, but absolutely has quirks as it is admittedly an experimental hack. It explodes if you use EF (initial testing did not). Use with caution. There’s also a comment from Marcin Dobosz if you’re interested in what the Web API team is looking at for OData support.
To demonstrate the problem, take a look at this Fiddler request. We ask for the total count of items, but the default ASP.NET Web API bits do not deliver the information!
[Aside: The merits of pagination are not the topic of this post. While I tend to agree with Jeff Atwood that paradigms are shifting, many still prefer - or are bound by their customers - to provide a page list].
One way around this was to simply make a second call to the server, parse out the $filter, and call Count(). This is not very elegant. While what I present below might not be the best possible approach, it does get us back down to a single call to the server which returns the $inlinecount as requested.
Let’s get to the code.
I chose to implement this as a message handler, so the first and simplest thing to do is register a new InlineCountHandler. I was working in a self-hosted Web API project, so I did this while setting up the configuration:
config.MessageHandlers.Add(new InlineCountHandler());
I had originally tried returning an anonymous type like this
new { Count = unpagedResults.Count(), Results = pagedResults }
// also tried pagedResults.ToArray() and a variety of other things
but this kept resulting in a 504. I decided to just go ahead and make a type to wrap our result. Trial and error also showed that our Result property needed to be an array (i.e., do not pass along the IQueryable) and cannot be object[] (i.e., we need to specify the proper type).
public class ResultValue<T>
{
public int Count { get; set; }
public T[] Results { get; set; }
}
After that it was just a matter of capturing the base response and adding the count in an override of SendAsync. If $inlinecount isn’t present and set to “allpages”, we just forward along the base response.
private bool ShouldInlineCount(HttpRequestMessage request)
{
var queryParams = request.RequestUri.ParseQueryString();
var inlinecount = queryParams["$inlinecount"];
return string.Compare(inlinecount, "allpages", true) == 0;
}
Otherwise, we press on by adding a continuation to the base task. We are only going to bother working our magic if the response was a 200 OK and resulted in an ObjectContent of IQueryable<>.
private bool ResponseIsValid(HttpResponseMessage response)
{
// Only do work if the response is OK
if (response == null || response.StatusCode != HttpStatusCode.OK) return false;
// Only do work if we are an ObjectContent
return response.Content is ObjectContent;
}
Type queriedType;
// Can we find the underlying type of the results?
if (pagedResultsValue is IQueryable)
queriedType = ((IQueryable)pagedResultsValue).ElementType;
else
return response;
I haven’t performance tested the following, but my theoretical understanding is we won’t be issuing any additional network traffic - this will just resend the modified request inside our own pipeline. Additionally, the additional database call should be limited to a SELECT COUNT, so not too much additional chatter. At any rate, it works, which is step one.
// Reissue the request without a skip/take to get our count. This will preserve filtering which
// could affect the count
var newRequest = new HttpRequestMessage(
request.Method,
request.RequestUri.AbsoluteUri.Replace("$skip=", "$_skip=").Replace("$top=", "$_top="));
// Get the result with no paging
var unpagedTaskResult = base.SendAsync(newRequest, cancellationToken).Result;
var unpagedResultsValue = this.GetValueFromObjectContent(unpagedTaskResult.Content);
We don’t know what custom types our controllers might end up delivering as the T in IQueryable<T>, so we get to play with some generics and dynamic invocation.
var resultsValueMethod =
this.GetType().GetMethod("CreateResultValue", BindingFlags.Instance | BindingFlags.NonPublic).MakeGenericMethod(new[] { queriedType });
// Create the result value with dynamic type
var resultValue = resultsValueMethod.Invoke(
this, new[] { unpagedResultsValue, pagedResultsValue });
Finally, we reset the Content to our ResultValue.
// Push the new content and return the response
response.Content = CreateObjectContent(resultValue, response.Content.Headers.ContentType);
return response;
So that’s what I came up with. You can get the full source code on GitHub. You’ll want the feature/inlinecount branch.
I’d be more than happy for someone to tear this apart and provide a better solution. Until then, it is what it is. I’ll almost certainly cover this at my upcoming ASP.NET Web API talk at HUNTUG in Huntsville, AL on 4/10/2012. Come out and see me!
Update: For whatever reason, this breaks content negotiation to XML. If you issue requests without $inlinecount with an Accept header for XML, it’s all groovy. Issue it with $inlinecount so that we intercept and add the Count, it still comes out as JSON. Be aware.
Update: XML Content Negotiation is fixed now. Silly me, forgot to use the ctor that preserves the MediaTypeHeaderValue. Other refactorings are also present in the latest code on GitHub, some of which has been incorporated into this post. Also note Marcin’s comment about the approach the Web API team is taking (still not slated for V1).
Another update: As it currently stands, this only works in a self hosted environment. If you host your Web API in an MVC website, for example, the base.SendAsync(newRequest… results in a 404 and bad things happen due to lack of error handling (hey, I told you this was not very elegant!). Anyway, Chris tells me he can get past the 404 by copying more than just the method and Uri to the request; however, his IOC breaks after that. You’ve been warned. ;) This has been fixed on the develop branch, although the general warning that this isn’t elegant stands. Proof of concept, experiment, hack; any of these terms are very reasonable.
And another: As Dante found, the “this has been fixed” part of the above update did eliminate  the exception when web hosting, but it also destroyed the accuracy. I have reverted the source on GitHub, and this will again only work in a self hosted environment.
This post originally appeared on The DevStop.