WebApi icon indicating copy to clipboard operation
WebApi copied to clipboard

Make it possible/easier to use LongCountAsync when $count=true

Open jr01 opened this issue 4 years ago • 2 comments

In our project we use Entity Framework Core 3.1 and throw when a synchronous DB call is detected (through a DbCommandInterceptor).

When doing a $count=true the oData framework sets request.ODataFeature().TotalCountFunc with Queryable.LongCount and executes that synchronously.

We have worked around the synchronous call by applying a custom [EnableQueryAsync] attribute instead of [EnableQuery] on the controller methods. See code below.

This workaround took a lot of effort and the oData library could perhaps provide an easier way.

Assemblies affected

Microsoft.AspNetCore.OData v7.5.0

Reproduce steps

GET /api/MyEntities$count=true

[EnableQuery]
public ActionResult<IQueryAble<MyEntity>> Get()
{
       var queryable = this.dbContext.MyEntities.AsQueryable();
       return this.Ok(queryable);
}

Expected result

In the future the oData library should make an asynchronous call when a provided async LongCount method is configured for a given IQueryProvider.

Actual result

A synchronous Queryable.LongCount call is made.

Additional detail

This is the custom attribute:

public class EnableQueryAsyncAttribute : EnableQueryAttribute
{
	public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
	{
		// note: don't call the base method. This method does the same as the base method in ActionFilterAttribute, except
		//       it executes await OnActionExecutedAsync(...) before executing this.OnActionExecuted(...)
		this.OnActionExecuting(context);
		if (context.Result == null)
		{
			var resultContext = await next().ConfigureAwait(false);
			await this.OnActionExecutedAsync(resultContext).ConfigureAwait(false);
		}
	}

	private async Task OnActionExecutedAsync(ActionExecutedContext resultContext)
	{
		if (resultContext.Result is ObjectResult objectResult &&
			objectResult.Value is IQueryable queryable &&
			queryable.Provider is IAsyncQueryProvider)
		{
			var request = resultContext.HttpContext.Request;
			var queryContext = new ODataQueryContext(request.GetModel(), queryable.ElementType, request.ODataFeature().Path);
			var queryOptions = new ODataQueryOptions(queryContext, request);

			if (queryOptions.Count.Value)
			{
				var filteredQueryable = (queryOptions.Filter == null ? queryable : queryOptions.Filter.ApplyTo(queryable, new ODataQuerySettings()))
                        as IQueryable<dynamic>;
				var cancellationToken = resultContext.HttpContext.RequestAborted;
				var count = await filteredQueryable.LongCountAsync(cancellationToken).ConfigureAwait(false);

				// Setting the TotalCount causes oData to not execute the TotalCountFunc.
				request.ODataFeature().TotalCount = count;
				if (count == 0)
				{
					// No need to have oData execute the queryable.
					var instance = Activator.CreateInstance(typeof(List<>).MakeGenericType(queryable.ElementType));
					resultContext.Result = new OkObjectResult(instance);
				}
			}
		}

		this.OnActionExecuted(resultContext);
	}
} 

I understand that the oData library can't know about the specific IQueryProvider's that's being used.

A nicer solution would be if we could configure the oData endpoint to use a specific asynchronous LongCount method for a given IQueryProvider in Startup.cs. Something like:

endPoints.
	.RegisterLongCountAsync<IAsyncQueryProvider>(QueryableMethods.LongCountWithoutPredicate);

and then the EnableQueryAttribute could use the registered LongCountAsync method and similar code as ^^^.

jr01 avatar Oct 14 '20 09:10 jr01

+1 on this issue. Its not currently possible to make queries OR counts asynchronous which is absolutely crucial for performance!

mbrankintrintech avatar Nov 21 '22 22:11 mbrankintrintech

Currently OData looks rather messy in terms of using sync / async execution, at any given time you can't be sure if you will end up with a synchronous operation. I stumbled across this issue completely randomly and I was absolutely clueless that something like it could even be a thing.

kerajel avatar Nov 25 '23 21:11 kerajel