Open jr01 opened 3 years ago
@jr01 we welcome a contribution on this. Feel free to do a PR and we'll be happy to review and merge. Thanks.
@jr01 Are you planning on submitting a PR for this? I also just noticed this and would be interested in this feature being added.
@wbuck @ElizabethOkerio The workaround is good enough for our current needs. Also we have been happy with a workaround for count async https://github.com/OData/WebApi/issues/2325 for about a year now ...
+1 on this issue.
@jr01 we added support for IAsncyEnumerable
using await foreach
in the ODataResourceSetSerializer
in the latest release of https://www.nuget.org/packages/Microsoft.AspNetCore.OData/. You could try it out and let us know if it works for you.
@ElizabethOkerio Does this mean that in the latest update, returning IAsyncQueryable
from the controller will make an async call to the database without any config changes?
If so, that's absolutely brilliant. kudos.
@ElizabethOkerio with the steps above and the latest release (8.2.3) the code still throws in BlockNonAsyncQueriesInterceptor
. Also I don't see any await foreach
in https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSetSerializer.cs ?
Wouldn't it be here instead?
I do see differences there.
Just decompiled 8.2.3 with dotpeek. Looks like that tag doesn't match with the published nuget.
This should be the right repo to look at: https://github.com/OData/AspNetCoreOData/blob/main/src/Microsoft.AspNetCore.OData/Formatter/Serialization/ODataResourceSetSerializer.cs
Having such a controller action method will make async calls.
[HttpGet]
[EnableQuery]
public IAsyncEnumerable<Customer> Get()
{
return this.context.Customers.AsAsyncEnumerable();
}
@ElizabethOkerio Is 8.2.3 the release with this in it?
yes.
@mbrankintrintech @jr01 we'd like feedback on this on whether it works as expected and if there are any improvements that we can make.
The big feedback for me would be that (if that this works) let's expand it to the other functions, namely the count command so that the count query is also non-blocking. 😄
Thanks. Yes, this is in our backlog.
@ElizabethOkerio I think I was mistaken before and I now do see the IAsyncEnumerable
code. Not sure what went wrong :)
Yes, the following is now making a non-blocking async call to the database, so that's great!:
[EnableQuery]
public IAsyncEnumerable<Person> Get()
{
var query = this.dbContext.Persons.AsAsyncEnumerable();
return query;
}
However, when returning an ActionResult with an encapsulated IAsyncEnumerable it still runs synchronously:
[EnableQuery]
public ActionResult<IAsyncEnumerable<Person>> Get()
{
var query = this.dbContext.Persons.AsAsyncEnumerable();
return this.Ok(query);
}
Maybe this would work when this part of the check wasn't done?:
writeContext.Type != null &&
writeContext.Type.IsGenericType &&
writeContext.Type.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)
Another thing that doesn't seem to work is an async IAsyncEnumerable<>
- this returns 400 badrequest -- the Get method isn't discovered/executed at all:
[EnableQuery]
public async IAsyncEnumerable<Person> Get([EnumeratorCancellation] CancellationToken cancellationToken)
{
await Task.Delay(10, cancellationToken);
var query = this.dbContext.Persons.AsAsyncEnumerable();
await foreach (var item in query)
{
yield return item;
}
}
@jr01 Thanks. I'll look into this.
@ElizabethOkerio I hope I'm not late, but according to the AsAsyncEnumerable implementation in EF Core:
public static IAsyncEnumerable<TSource> AsAsyncEnumerable<TSource>(
this IQueryable<TSource> source)
{
Check.NotNull(source, nameof(source));
if (source is IAsyncEnumerable<TSource> asyncEnumerable)
{
return asyncEnumerable;
}
throw new InvalidOperationException(CoreStrings.IQueryableNotAsync(typeof(TSource)));
}
The object representing EF Core's IQueryable
I believe this is a critical enhancement because query execution times can extend to tens of seconds or even minutes in practical scenarios. Prolonged synchronous queries can lead to thread starvation. Thus, implementing this straightforward check can significantly boost OData performance in real-world use cases.
@DimaMegaMan I could be wrong here but I don't think the source System.Linq.IQueryable
implements IAsyncEnumerable
. I would also like to understand whether you mean that having such
[EnableQuery]
public IQueryable<Person> Get()
{
return this.dbContext.Persons;
}
should lead to async database calls being made? Currently, If you return an IQueryable
that implements IAsyncEnumerable
then async database calls are made..
I think I have an explanation of what might be happening with this not working by default in the aspnet core version of this here https://github.com/OData/AspNetCoreOData/issues/1194
I'm using the [EnableQuery] attribute inside a normal [ApiController].
This works when I return a Task<IQueryable
System.ArgumentNullException: Value cannot be null. (Parameter 'entityClrType')
at Microsoft.AspNetCore.OData.Extensions.ActionDescriptorExtensions.GetEdmModel(ActionDescriptor actionDescriptor, HttpRequest request, Type entityClrType)
at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.GetModel(Type elementClrType, HttpRequest request, ActionDescriptor actionDescriptor)
at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.CreateQueryOptionsOnExecuting(ActionExecutingContext actionExecutingContext)
at Microsoft.AspNetCore.OData.Query.EnableQueryAttribute.OnActionExecuting(ActionExecutingContext actionExecutingContext)
at Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute.OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
I'm not defining EdmModels or ClrTypes anywhere for the IQueryable code, so why would returning IAsyncEnumerable suddenly complain about it?
@aldrashan We'll look into this.
I've been investigating the available options for improving the performance of OData for large data sets. Essentially, I hit a brick wall due to this issue.
From what I'm seeing, the root cause is the use of TruncatedCollection
, which forces the query to be both synchronous and completely held in RAM. For large data volumes, this uses gigabytes of server memory, even with relatively small page sizes.
Conversely, the recently merged support for IAsyncEnumerable<>
has great performance with a constant amount of very low memory usage. However, it is never used by EnableQuery, because it implicitly converts EF IQueryable
data into this truncated collection instead of an enumerable.
A possible solution might be to replace TruncatedCollection
with TruncatedEnumerable
or something similar that allows async enumeration of large data sets.
On .Net 5 / MVC 5 + EF Core 5 when an controller action with
[EnableQuery]
returned an EF CoreIQuerable
which implementedIAsyncEnumerable
the database call would be executed asynchronously.With .Net 6 / MVC 6 + EF Core 6 the database call is executed synchronously.
Assemblies affected
Microsoft.AspNetCore.OData 8.0.4
Reproduce steps
In an ASP.Net 6 + oData 8.0.4 + EF core 6 project:
Expected result
Asynchronous DB calls are made.
Actual result
A synchronous DB call is made - not good for scaling/performance. With the
BlockNonAsyncQueriesInterceptor
the exception occurs at the foreach loop here: https://github.com/OData/WebApi/blob/master/src/Microsoft.AspNet.OData.Shared/Formatter/Serialization/ODataResourceSetSerializer.cs#L230Additional detail
I'm not sure if this is related at all, but I found MVC 5 buffered
IAsyncEnumerable
and this got removed in MVC 6 - https://docs.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/6.0/iasyncenumerable-not-buffered-by-mvcAnyway I found a workaround by introducing an
EnableQueryAsync
attribute and execute theIAsyncEnumerable
and buffer the results in memory:Maybe a solution is if
ODataResourceSetSerializer
and other collection serializers check ifIEnumerable
is anIAsyncEnumerable
and performs anawait foreach
?