Supporting OData $inlinecount & json verbose with Web API OData

OData, the open data protocol is an awesome protocol for exposing data from your server tier, because it allows the caller to use special query arguments to filter, sort, select only particular columns, request related entities in a single call, and do paging.

Basically, this means you end up with an “open” data service API, where you literally just expose data and leave it up to the client to dictate the specific use case. Whether you want to do that is kinda negotiable for your own client, but when you’re building an application where you want to really want to support and nurture users building 3rd party integration tools, OData is the perfect candidate to build an “I don’t know beforehand what scenarios you want to accomplish” API.

Furthermore, creating an OData read service is really simple, you take the Microsoft.AspNet.WebApi.OData nuget package, you expose an IQueryable in your Controller, and you slap on the [EnableQueryAttribute]:

    public class QueryController : ApiController
    {

        [EnableQuery]         
        [HttpGet]
        public IQueryable People()
        {
            return this.dbContext.People;
        }
    }

So here’s the problem: suppose there’s 100 people in the database, with ages evenly divided from 1 – 100. The caller requests all people with age > 50 ($filter=age gt 50). We also applied a page-size (which you should really always do to avoid self-inflicted DDOS attacks) of 25 maximum records in a single response. At this point, we do not want to just send back 25 records, but we also want to inform the caller that we have a applied a page-size and there are really 50 people that match his search criteria, and wouldn’t it be nice if we can also inform the caller how to get the next page?

The good news is: according to the OData spec, you can! By returning an “OData verbose” response (“verbose” being the opposite of “light”, which is the new OData default response), you can send back a result not only containing the actual results but additional metadata like the number of people that matched your search criteria, and how to get the next page of results.

The really bad news is: the Web API OData implementation does not support the $inlinecount query parameter (which instructs the server to send back the count after filtering but before paging). OUCH!

Weirdly, after following a dozen blog posts (like this really good one ) I stumbled upon the fact that this is only partly true… The Web API Odata implementation does in fact support the $inlinecount query parameter, however it does not in any way support actually sending back the JSON verbose format where the caller actually gets to see the query parameter…
Wait, whot?
A caller can send the $inlinecount, the EnableQueryAtrribute (which really does all the heavy work) will correctly handle it, but instead of properly sending the count to the client it will simply keep it in memory and send only the results back. Same story with the link to the next page of records, when you implement a PageSize.
So the good news is: to re-enable the $inlinecount, or in other words: send back a more verbose response to the user, you can make your own EnableQueryAttribute:

using Newtonsoft.Json;
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Http.Filters;
using System.Web.Http.OData;
using System.Web.Http.OData.Extensions;
using System.Web.Http.OData.Query;

namespace Lobsta.webapi
{
    internal class ODataVerbose
    {
        public IQueryable Results { get; set; }

        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
        public long? __count { get; set; }

        [JsonProperty(NullValueHandling = NullValueHandling.Ignore)]
        public string __next { get; set; }
    }
    public class QueryableAttribute : EnableQueryAttribute
    {
        public bool ForceInlineCount { get; private set; } 
        public QueryableAttribute(bool forceInlineCount = true, int PageSize = 25)
        {
            this.ForceInlineCount = forceInlineCount;
            //Enables server paging by default
            if (this.PageSize == 0)
            {
                this.PageSize = PageSize;
            }
        }
        public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
        {
            //Enables inlinecount by default if forced to do so by adding to query string
            if (this.ForceInlineCount && !actionExecutedContext.Request.GetQueryNameValuePairs().Any(c => c.Key == "inlinecount"))
            {
                var requestUri = actionExecutedContext.Request.RequestUri.ToString();
                if (string.IsNullOrEmpty(actionExecutedContext.Request.RequestUri.Query))
                    requestUri += "?$inlinecount=allpages";
                else
                    requestUri += "&$inlinecount=allpages";
                actionExecutedContext.Request.RequestUri = new Uri(requestUri); 
            }

            //Let OData implementation handle everything
            base.OnActionExecuted(actionExecutedContext);

            //Examine if we want to return fat result instead of default
            var odataOptions = actionExecutedContext.Request.ODataProperties();  //This is the secret sauce, really.
            object responseObject;
            if (
                ResponseIsValid(actionExecutedContext.Response) 
                && actionExecutedContext.Response.TryGetContentValue(out responseObject)
                && responseObject is IQueryable)
            {
                actionExecutedContext.Response =
                    actionExecutedContext.Request.CreateResponse(
                        HttpStatusCode.OK,
                        new ODataVerbose
                        {
                            Results = (IQueryable)responseObject,
                            __count = odataOptions.TotalCount,
                            __next = (odataOptions.NextLink == null) ? null : odataOptions.NextLink.PathAndQuery
                        }
                    );
            }
        }

        private bool ResponseIsValid(HttpResponseMessage response)
        {
            return (response != null && response.StatusCode == HttpStatusCode.OK && (response.Content is ObjectContent));
        }
    }
}

Note: this is highly opinionated sample code, it always uses a page size of 25, and always returns the inlinecount… Change to your liking, by example checking if the requested format is jsonverbose, to be OData spec compliant
Finally, replace the ‘EnableQuery’ attribute with our custom one:

    public class QueryController : ApiController
    {

        [Queryable]         
        [HttpGet]
        public IQueryable People()
        {
            return this.dbContext.People;
        }
    }

Putting it to the test, I called: /api/query/people?$orderby=name&$filter=age gt 50&$inlinecount=allpages again and now correctly receive my requested metadata:

{
 "Results":[
   {"age":51,"name":"Anna"} /* More results were included of course */
 ],
 "__count":50,
 "__next":"/api/query/people?$orderby=name&$filter=age%20gt%2050$inlinecount=allpages&$skip=25"
}
Advertisements

6 thoughts on “Supporting OData $inlinecount & json verbose with Web API OData

      • Hey Josh,

        indeed, the weird thing is that it is fixed in the way that the Web API Odata implementation does calculate the count&nextpage when needed to. However, there’s no way to send that back to the client. $format=odataverbose, accept headers, none of it works, you only get JSON Light back 😦

        Additionally, even if the implementation would support sending back JSON verbose with that metadata by adding query strings or content headers, it’s no longer the default response. A lot of controls (Devexpress, Telerik, BreezeJS, 3rd party libs) either needed to be updated or now no longer work because of this breaking API change. With the ‘hack’ above, you can have your service send back the JSON verbose by default.

        And, in my opinion, JSON verbose is a way better standard than JSON light, but that’s a different discussion 🙂

        No idea about the ODataController, but I suspect there’s no reason for it to use a different implemenation than the EnableQueryAttribute’s one, which is in the same package.

        Keep rocking! 😉

        Jan

  1. Was this really posted on April 30th this year?

    I’m trying your approach and really hope is an updated solution. I am currently facing a problem, everytime I try to send the request like this: http://localhost:5508/Passengers?%24inlinecount=allpages&%24format=json&%24top=2

    I get: The query parameter ‘$inlinecount’ is not supported.

    I used your “Queryable” attribute too, but it didn’t help me. The problem is even bigger when I use it, because everytime I call the service it returns the same error shown above even when I didn’t provide the ‘$inlinecount’ parameter.

    Hope you can help me and thanks for the article.

  2. And in case you control the client and want to send pure lite JSON you can send additional info in http header.
    This in turn simplifies class a lot since you don’t have to mess with a response.

    Public Class EnableQueryWithPagingAttribute
    Inherits EnableQueryAttribute

    Public Overrides Sub OnActionExecuted(actionExecutedContext As HttpActionExecutedContext)
    
        If Not actionExecutedContext.Request.GetQueryNameValuePairs().Any(Function(c) c.Key = "inlinecount") Then
            Dim requestUri = actionExecutedContext.Request.RequestUri.ToString()
            If (String.IsNullOrEmpty(actionExecutedContext.Request.RequestUri.Query)) Then
                requestUri &= "?$inlinecount=allpages"
            Else
                requestUri &= "&$inlinecount=allpages"
            End If
            actionExecutedContext.Request.RequestUri = New Uri(requestUri)
        End If
    
        MyBase.OnActionExecuted(actionExecutedContext)
        Dim odataOptions = actionExecutedContext.Request.ODataProperties()
        If odataOptions.TotalCount.HasValue Then
            HttpContext.Current.Response.AddHeader("X-totalcount", odataOptions.TotalCount.Value.ToString())
        End If
    
    End Sub
    

    End Class

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s