Items count in OData v4 WebAPI response

asp.net-web-apicountodata

How to return number of items in OData v4 HTTP response?

I need this number to pagination, so it should be number of items after filtering, but before 'skip' and 'top'.

I already tried passing '$inlinecount=allpages' and '$count=true' parameters in query options in url (https://damienbod.wordpress.com/2014/06/13/web-api-and-odata-v4-queries-functions-and-attribute-routing-part-2/ – "Example of $count"), but my responses from WebAPI always have only query results (collection) – whole response looks like:

[
    {
        "Name":"name1", 
        "age":5
    }, 
    {
        "Name":"name2", 
        "age":15
    }
]

There is nothing like "odata.count" in the response.

I also tried returning PageResult instead of IQueryable in my WebAPI controller action (like described here: http://www.asp.net/web-api/overview/odata-support-in-aspnet-web-api/supporting-odata-query-options#server-paging), but Request.GetInlineCount() is deprecated and its value is always null.

Any ideas?

[Update] I just found the same problem here: WebApi with Odata NextPage and Count not appearing in the JSON response and I removed [EnableQuery] attribute and now my response looks like:

{
    "Items":
    [
        {
            "Name":"name1", 
            "age":5
        }, 
        {
            "Name":"name2", 
            "age":15
        }
    ],
    "NextPageLink":null,
    "Count":null
}

But still "Count" is always null. 🙁


Edit: After debugging and searching for count value in Request properties in my controller, I found out that correct Count value is in property named "System.Web.OData.TotalCount". So right now I exctract this value from that request property and my controller looks like that:

public PageResult<People> Get(ODataQueryOptions<People> queryOptions)
{
    var query = _context.People.OrderBy(x => x.SomeProperty);
    var queryResults = (IQueryable<People>)queryOptions.ApplyTo(query);
    long cnt = 0;
    if (queryOptions.Count != null)
        cnt = long.Parse(Request.Properties["System.Web.OData.TotalCount"].ToString());

    return new PageResult<People>(queryResults, null, cnt);
}

And it works fine, but I still don't know why I have to use workarounds like that.

Best Answer

For future reference (OData v4):

First of all $inlinecount it's not supported in OData v4 so you should use $count=true instead.

Second, if you have a normal ApiController and you return a type like IQueryable<T> this is the way you can attach a count property to the returned result:

using System.Web.OData;
using System.Web.OData.Query;
using System.Web.OData.Extensions;

//[EnableQuery] // -> If you enable globally queries does not require this decorator!
public IHttpActionResult Get(ODataQueryOptions<People> queryOptions)
{
    var query = _peopleService.GetAllAsQueryable(); //Abstracted from the implementation of db access. Just returns IQueryable<People>
    var queryResults = (IQueryable<People>)queryOptions.ApplyTo(query);
    return Ok(new PageResult<People>(queryResults, Request.ODataProperties().NextLink, Request.ODataProperties().TotalCount));
}

Note: OData functionality does not supported by ApiControllers so you cannot have things like count or $metadata. If you choose to use simple ApiController the way above is the one you should use to return a count property.


For a full support of OData functionality you should implement a ODataController the following way:

PeopleController.cs

using System.Web.OData;
using System.Web.OData.Query;

public class PeopleController : ODataController
{
    [EnableQuery(PageSize = 10, AllowedQueryOptions = AllowedQueryOptions.All)]
    public IHttpActionResult Get()
    {
        var res = _peopleService.GetAllAsQueryable();
        return Ok(res);
    }
}

App_Start \ WebApiConfig.cs

public static void ConfigureOData(HttpConfiguration config)
{
    //OData Models
    config.MapODataServiceRoute(routeName: "odata", routePrefix: null, model: GetEdmModel(), batchHandler: new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer));
    config.EnsureInitialized();
}

private static IEdmModel GetEdmModel()
{
    var builder = new ODataConventionModelBuilder
    {
        Namespace = "Api",
        ContainerName = "DefaultContainer"
    };
    builder.EntitySet<People>("People").EntityType.HasKey(item => item.Id); //I suppose the returning list have a primary key property(feel free to replace the Id key with your key like email or whatever)
    var edmModel = builder.GetEdmModel();
    return edmModel;
}

Then you access your OData Api this way (example):

encoded uri:

http://localhost:<portnumber>/People/?%24count=true&%24skip=1&%24top=3

decoded:

http://localhost:<portnumber>/People/?$count=true&$skip=1&$top=3

References:

Related Topic