Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WebAPI OData $Skip on custom IQueryable double applied

I have implemented a custom IQueryable that is exposed via a WebAPI OData endpoint. The structure of the controller's Get() is rather standard:

[EnableQuery(
    AllowedQueryOptions = AllowedQueryOptions.Count
                          | AllowedQueryOptions.Filter
                          | AllowedQueryOptions.OrderBy
                          | AllowedQueryOptions.Skip
                          | AllowedQueryOptions.Top)]
[ODataRoute]
public PageResult<Foo> Get(ODataQueryOptions<Foo> queryOptions)
{

    var bars = new QueryableData<Foo>(_provider);

    var result = ((IQueryable<Foo>)queryOptions
        .ApplyTo(bars,
            new ODataQuerySettings(new ODataQuerySettings { EnableConstantParameterization = false, EnsureStableOrdering = false }))).ToList();
    var count = _provider.Count;
    return new PageResult<Foo>(result, null, count);
}

The odd behavior I am seeing, is that an OData $Skip in the query string is applied after the PageResult is returned. For example:

  • if the query string contains a ?$top=10&$skip=10 there will be no results return.
  • if the query string contains a ?&top=12&skip=10 there will be (2) results returned.

What I am looking to do is prevent the framework(s) from applying the Skip to my results set since the query provider is already implementing the skip. Are there ODataQuerySettings that can be set to prevent this double application of the skip?

EDIT: Upon further investigation, when I remove $count=true from the query string skip (and top) function as expected. This leads me to believe that my approach to implementing $count=true is incorrect. From my debugging sessions it appears that when $count=true is in the query options the queryable has the expression tree applied to it twice, once with a return type of long, and then again without the wrapping countlong expression. I have tried returning the count on the first pass and then proper queryable for the second pass, but this results in the delayed application of the skip expression. There seems be be something very fundamental that I am missing here.

like image 906
Bill Berry Avatar asked Jun 03 '15 00:06

Bill Berry


2 Answers

While reading through the Github issues list I came across this post: OData PageResult method ignoring count parameter when using EnableQuery attribute #159. What appears to be the problem is the combination of EnableQuery Attribute and the parameterized Get constructor taking the ODataQueryOptions. Using both means that you will implement the constructor query options, applying the query expressions, then the framework will apply what filters it can on direction from the applied attribute; therefore double applying things like skip, top and orderby.

like image 78
Bill Berry Avatar answered Oct 12 '22 23:10

Bill Berry


Since I wanted to handle the skip myself but I still wanted the other features of the EnableQueryAttribute I had a look and noticed it was virtual. I created a derived class and then attempted to override the ApplyQuery methods. Unfortunately the ODataQueryOptions only had private sets on its properties so I ninja-ed in a little reflection. (It feels dirty but hey..)

public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
    var skipOption = new SkipQueryOption("0", queryOptions.Context);
    typeof(ODataQueryOptions).GetProperty("Skip").SetValue(queryOptions, skipOption, null);

    return base.ApplyQuery(queryable, queryOptions);
}

With the skip option now being 0 it doesn't apply it when constructing the response and no more "double skip blues".

like image 40
slamb2k Avatar answered Oct 12 '22 23:10

slamb2k