AspNetCoreOData icon indicating copy to clipboard operation
AspNetCoreOData copied to clipboard

TotalCountFunc evaluated before IQueryable is enumerated

Open davhdavh opened this issue 3 years ago • 6 comments

ODataResourceSetSerializer.CreateResourceSet executes the TotalCountFunc, but that is called before the IQueryable is evaluated in ODataResourceSetSerializer.WriteResourceSetAsync and thus the TotalCountFunc can never know anything about what filters were requested, and thus always returns the wrong result when using TotalCountFunc.

resourceSetInstance.GetEnumerator() MUST be called before executing TotalCountFunc!

This is a critical problem.

davhdavh avatar Mar 08 '22 09:03 davhdavh

Can you provide an actual sample of what you are talking about?

julealgon avatar Mar 08 '22 12:03 julealgon

@davhdavh Please note that TotalCountFunc should be called after applying filter but before applying skip and top. If you're observing a different behaviour please let us know so we can investigate further

gathogojr avatar Mar 08 '22 17:03 gathogojr

https://localhost:7003/api/odata/Test should return count 2 or higher... But it returns 1.

using System.Collections;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.OData.Extensions;
using Microsoft.AspNetCore.OData.Query;
using Microsoft.AspNetCore.OData.Routing.Controllers;

namespace WebApplication7.Controllers;

public class TestController : ODataController
{
   public static MyState JustForTestingState = MyState.Startup;
   [HttpGet, EnableQuery]
   public IQueryable<Test> Get()
   {
      JustForTestingState = MyState.Startup;
      Request.ODataFeature().TotalCountFunc = TotalCountFunc;
      JustForTestingState = MyState.AfterTotalFunc;
      return new MyQuerable(new());
   }

   private long TotalCountFunc() => (int)JustForTestingState;
}

public enum MyState
{
   Startup = 0,
   AfterTotalFunc,
   AfterGetEnumerator,
   AfterIterationStart,
   AfterIterationEnd
}

public class MyIQueryProvider : IQueryProvider
{
   public IQueryable CreateQuery(Expression expression) => throw new NotImplementedException();

   public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
   {
      if (typeof(TElement) != typeof(Test)) throw new NotImplementedException();
      return (IQueryable<TElement>)new MyQuerable(this, expression);
   }

   public object? Execute(Expression expression) => throw new NotImplementedException();

   public TResult Execute<TResult>(Expression expression)=> throw new NotImplementedException();
}



public class MyQuerable : IQueryable<Test>
{
   private readonly MyIQueryProvider _provider;

   public MyQuerable(MyIQueryProvider provider)
   {
      _provider  = provider;
      Expression = Expression.Constant(this);
   }

   public MyQuerable(MyIQueryProvider provider, Expression expression)
   {
      if (!typeof(IQueryable<Test>).IsAssignableFrom(expression.Type)) throw new ArgumentOutOfRangeException(nameof(expression));
      _provider  = provider;
      Expression = expression;
   }

   public Expression Expression { get; }

   public Type ElementType => typeof(Test);

   public IQueryProvider Provider => _provider;

   public IEnumerator<Test> GetEnumerator()
   {
      TestController.JustForTestingState = MyState.AfterGetEnumerator;
      return new MyEnumerator();
   }

   IEnumerator IEnumerable.GetEnumerator() => _provider.Execute<IEnumerable>(Expression).GetEnumerator();
}

public class MyEnumerator : IEnumerator<Test>
{
   private bool first = true;

   public bool MoveNext()
   {
      if (!first)
      {
         TestController.JustForTestingState = MyState.AfterIterationEnd;
         return false;
      }

      TestController.JustForTestingState = MyState.AfterIterationStart;
      first = false;
      return true;
   }

   public void Reset()
   {
   }

   public Test Current => new() { Id = 42, Name = "hello" };

   object IEnumerator.Current => Current;

   public void Dispose()
   {
   }
}

public class Test
{
   public int Id { get; set; }
   public string Name { get; set; } = "";
}

davhdavh avatar Mar 09 '22 04:03 davhdavh

Also for the same reason, this really should show an error because the queryprovider isnt actually implemented... Instead it just returns a partial json result.

davhdavh avatar Mar 09 '22 04:03 davhdavh

Don't you have a simpler repro scenario that you could share @davhdavh ? Your sample above has a ton of custom stuff in it.

julealgon avatar Mar 09 '22 13:03 julealgon

Ok... Same thing, still should return count=2 in the json, but returns count=1.

public class TestController : ODataController
{
   public static MyState JustForTestingState = MyState.Startup;
   [HttpGet, EnableQuery]
   public IQueryable<Test> Get()
   {
      JustForTestingState = MyState.Startup;
      Request.ODataFeature().TotalCountFunc = TotalCountFunc;
      JustForTestingState = MyState.AfterTotalFunc;
      return new MyEnumerable().AsQueryable();
   }

   private long TotalCountFunc() => (int)JustForTestingState;
}

public class MyEnumerable : IEnumerable<Test>
{
   public IEnumerator<Test> GetEnumerator()
   {
      TestController.JustForTestingState = MyState.AfterGetEnumerator;
      return new List<Test> { new() { Id = 42, Name = "hello" } }.GetEnumerator();
   }

   IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}


public enum MyState
{
   Startup = 0,
   AfterTotalFunc,
   AfterGetEnumerator,
}

davhdavh avatar Mar 10 '22 04:03 davhdavh