13
votes

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.

4
It would be helpful if you could attach the controller method you wrote for getting the entity set.Yi Ding - MSFT

4 Answers

11
votes

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:

3
votes

This can also be achieved by an action filter:

/// <summary>
/// Use this attribute whenever total number of records needs to be returned in the response in order to perform paging related operations at client side.
/// </summary>
public class PagedResultAttribute: ActionFilterAttribute
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="actionExecutedContext"></param>
    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        base.OnActionExecuted(actionExecutedContext);
        if (actionExecutedContext.Response != null)
        {                
            dynamic responseContent=null;
            if (actionExecutedContext.Response.Content != null)
                responseContent = actionExecutedContext.Response.Content.ReadAsAsync<dynamic>().Result;
            var count = actionExecutedContext.Response.RequestMessage.ODataProperties().TotalCount;
            var res = new PageResult<dynamic>() {TotalCount=count,Items= responseContent };

            HttpResponseMessage message = new HttpResponseMessage();
            message.StatusCode = actionExecutedContext.Response.StatusCode;

            var strMessage = new StringContent(JsonConvert.SerializeObject(res), Encoding.UTF8, "application/json");
            message.Content = strMessage;
            actionExecutedContext.Response = message;               
        }           
    }
}

And the custom PageResult class is:

public class PageResult<T>
{      
    public long? TotalCount { get; set; }
    public T Items { get; set; }
}

Usage:

[PagedResult]
[EnableQuery()]  
1
votes

Will you please take a look at the sample service TripPin web api implementation at https://github.com/OData/ODataSamples/blob/master/Scenarios/TripPin. You can follow the code in Airports controller and the service with the code http://services.odata.org/TripPinWebApiService/Airports?$count=true can return the count correctly.

1
votes

That's what I am using with oData v4:

Request.ODataProperties().NextLink, 

Request.ODataProperties().TotalCount