$count
is actually a part of OData v3 specification, which is similar to $inlinecount
of OData v2 specification. $count
must return a single integer value in the response. $inlinecount
will return total count of entity set along with the entity set itself in the response. Both have their uses, however when working with SAPUI5, $count
is expected to be supported by default. Which is interesting, because SAP NetWeaver Gateway only produces OData v2 services. Regardless, it is a feature that we need to implement. Let's take a look.
It is possible to turn off $count requests in SAPUI5 by setting your OData Model default count mode. (
oDataModel.setDefaultCoundMode("x")
, wherex
isInline
,Request
,Both
, orNone
). But for this lab, let's pretend we can't change it and we are required to support it.
https://server.com/odata/Todos/$count
Response (200 OK, text/plain):
2
In Web API OData, you can implement your own OData path handler which implements the IODataPathHandler
interface. In this way, we can take over the handler to check if our valid route is $count
, then simply return an integer value of the entity set length. This can be set as a property of MapODataServiceRoute
in WebApiConfig
which we added earlier when registering our OData routes.
CountPathSegment.cs
In order to have a valid path segment, we must declare a type which inherits from ODataPathSegment
. This custom path segment will let Web API know information about the path segment name ($count
), IEdmType
(Edm.Int32
, per OData v3 specification), and entity set (derive from controller).
using Microsoft.Data.Edm;
using Microsoft.Data.Edm.Library;
using System.Web.Http.OData.Routing;
namespace ODataLab1.Helpers
{
public class CountPathSegment : ODataPathSegment
{
public override string SegmentKind
{
get
{
return "$count";
}
}
public override IEdmType GetEdmType(IEdmType previousEdmType)
{
return EdmCoreModel.Instance.FindDeclaredType("Edm.Int32");
}
public override IEdmEntitySet GetEntitySet(IEdmEntitySet previousEntitySet)
{
return previousEntitySet;
}
public override string ToString()
{
return "$count";
}
}
}
Now that we have the path segment, we need to add a path handler.
CountODataPathHandler.cs
The path handler will hook into the default OData path handler and will recognize the "$count
" segment, and return an instance of our above ODataPathSegment
. If it sees anything other than "$count
" as the segment name, it will pass through to the base method, as you would expect.
using Microsoft.Data.Edm;
using System.Web.Http.OData.Routing;
namespace ODataLab1.Helpers
{
public class CountODataPathHandler : DefaultODataPathHandler
{
protected override ODataPathSegment ParseAtEntityCollection(IEdmModel model, ODataPathSegment previous, IEdmType previousEdmType, string segment)
{
if (segment == "$count")
{
return new CountPathSegment();
}
return base.ParseAtEntityCollection(model, previous, previousEdmType, segment);
}
}
}
Finally, we need a custom routing convention to tie this custom segment in with a controller/action.
CountODataRoutingConvention.cs
A custom routing convention maps a route to an OData Controller and an Action. In this case, our route is "$count
", our controller would be specified in the URI path (https://server.com/odata/Products/$count
), and our action will need to be called GetCount
. We can specify any name, but it will have to be available in every entity. Let's extend EntitySetRoutingConvention
and override SelectAction()
.
using System.Linq;
using System.Net.Http;
using System.Web.Http.Controllers;
using System.Web.Http.OData.Routing;
using System.Web.Http.OData.Routing.Conventions;
namespace ODataLab1.Helpers
{
public class CountODataRoutingConvention : EntitySetRoutingConvention
{
public override string SelectAction(ODataPath odataPath, HttpControllerContext controllerContext, ILookup<string, HttpActionDescriptor> actionMap)
{
if (controllerContext.Request.Method == HttpMethod.Get && odataPath.PathTemplate == "~/entityset/$count")
{
if (actionMap.Contains("GetCount"))
{
return "GetCount";
}
}
return null; // let default routes handle
}
}
}
Now Web API knows which method to call when it sees the /$count
segment. Fantastic.
Now that we have a custom routing convention, let's register it in WebApiConfig
.
ODataRoutingConventions
. Add this line right before the call to config.Routes.MapODataServiceRoute
.IList<IODataRoutingConvention> conventions = ODataRoutingConventions.CreateDefault();
conventions.Insert(0, new CountODataRoutingConvention()); // allow $count segments in WebAPI OData v1-3
Tip : if you see errors when typing classes which are not yet in your using clauses, click in the red area in Visual Studio and press
[Control + .]
- you can then hit enter to let Visual Studio add the reference for you. Handy!
CountODataPathHandler
into our MapODataServiceRoute
call. Add these additional parameters to the function call.config.Routes.MapODataServiceRoute("odata", "odata", GetModel(), new CountODataPathHandler(), conventions);
Before we can run the project, we still need to implement our GetCount
method in the TodosController
. Remember, our custom routing convention determines the action name that is called. Add this action in your TodosController
:
public HttpResponseMessage GetCount(ODataQueryOptions<Todo> queryOptions)
{
IQueryable<Todo> queryResults = queryOptions.ApplyTo(GetTodos()) as IQueryable<Todo>;
int count = queryResults.Count();
HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.OK);
response.Content = new StringContent(count.ToString(), Encoding.UTF8, "text/plain");
return response;
}
Let's build the project and run it again. Try:
http://localhost:your-port-number/odata/Todos/$count
Now you should get a text/plain response with an integer value. Great! We did it! For any new ODataControllers, you will simply need to implement GetCount
action to support /$count
path.