7
votes

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.

4

4 Answers

8
votes

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.

3
votes

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".

1
votes

I have found that if you are using swagger/swashbuckle, removing the attribute all together will cause some of the OData options to not be generated by swashbuckle. Creating your own attribute that overrides the ApplyQuery method to do nothing and just return the original query works as expected.

    // OData framework's EnableQuery attribute will apply query's again, after we have already applied the query to the result set
    // (So For e.g. you will get Top and Skip applied again on your results that have already had top and skip applied
    // this is a workaround the disables client side queries until this is fixed.
    // https://github.com/OData/WebApi/issues/159
    public class EnableCustomQueryAttribute : EnableQueryAttribute
    {
        public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
        {
            return queryable;
        }
    }
0
votes

Based on slamb2k's response, a bit more up to date version:

public override IQueryable ApplyQuery(IQueryable queryable, ODataQueryOptions queryOptions)
{
    var parser = typeof(ODataQueryOptions).GetField("_queryOptionParser", BindingFlags.Instance | BindingFlags.NonPublic).GetValue(queryOptions) as ODataQueryOptionParser;
    typeof(ODataQueryOptions).GetProperty("Skip").SetValue(queryOptions, new SkipQueryOption("0", queryOptions.Context, parser), null);
    typeof(ODataQueryOptions).GetProperty("Top").SetValue(queryOptions, new TopQueryOption("0", queryOptions.Context, parser), null);
    typeof(ODataQueryOptions).GetProperty("OrderBy").SetValue(queryOptions, new OrderByQueryOption("0", queryOptions.Context, parser), null);

    return base.ApplyQuery(queryable, queryOptions);
}