Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
jr01 opened this issue Oct 14, 2020 · 2 comments
Open

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

jr01 opened this issue Oct 14, 2020 · 2 comments
Assignees

Comments

@jr01
Copy link

jr01 commented Oct 14, 2020

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

@mbrankintrintech
Copy link

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

@kerajel
Copy link

kerajel commented Nov 25, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants