Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Items count in OData v4 WebAPI response

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.

like image 994
anaid Avatar asked Dec 18 '14 11:12

anaid


People also ask

What is $select in OData?

The $select option specifies a subset of properties to include in the response body. For example, to get only the name and price of each product, use the following query: Console Copy. GET http://localhost/odata/Products?$select=Price,Name.

How do I filter OData query?

You can use filter expressions in OData requests to filter and return only those results that match the expressions specified. You do this by adding the $filter system query option to the end of the OData request.

What is Odataqueryoptions?

OData defines parameters that can be used to modify an OData query. The client sends these parameters in the query string of the request URI. For example, to sort the results, a client uses the $orderby parameter: http://localhost/Products?$orderby=Name. The OData specification calls these parameters query options.


3 Answers

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()]  
like image 106
Vikrant Verma Avatar answered Oct 22 '22 09:10

Vikrant Verma


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:

  • How to Use Web API OData to Build an OData V4 Service without Entity Framework
  • Web API OData V4 Pitfalls
  • Create an OData v4 Endpoint Using ASP.NET Web API 2.2
like image 27
CodeArtist Avatar answered Oct 22 '22 10:10

CodeArtist


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.

like image 22
QianLi Avatar answered Oct 22 '22 10:10

QianLi