Cannot avoid using synchronous calls in OData with entity framework
@ElizabethOkerio is already working on this.
Since Mar 19, 2024.
- Dominant language
- C#
- Stars
- 505
- Forks
- 186
- PR merge metrics
- No merged PRs in 30d
Description
Assemblies affected
Microsoft.AspNetCore.OData 8.2.5 (but probably all of them
Describe the bug
When using an IQueryable generated via entity framework the default behaviour is to have synchronous calls to the database rather than using it as an IAsyncEnumerable to avoid this, there is a workaround for this (described below) but I think the default behaviour should be to do this Asynchronously. Also if you add $count=true then the counting will always happen synchronously and it's not even obvious what sort of workaround there would be.
Reproduction steps
Checkout the repo https://github.com/Shiney/AsyncEfCoreOdata/blob/main/AsyncEfCoreOdata/Controllers/Controller.cs here. Then run it, which should go to .../Odata/Customers/$filter=Id%20eq%201%20or%20Id%20eq%203&$count=true
This will then throw an error because I've configured non async DbDataReaders to not work.
The crucial code looks like this
/// <summary>
/// Throw an error if the reader is not async
/// </summary>
private class ReaderInterceptor : DbCommandInterceptor
{
public override DbDataReader ReaderExecuted(DbCommand command, CommandExecutedEventData eventData, DbDataReader result)
{
throw new InvalidOperationException("Expect all calls to read to be async");
}
}
[EnableQuery]
public async Task<ActionResult> Get()
{
var options = new DbContextOptionsBuilder<MyDbContext>()
.AddInterceptors(new ReaderInterceptor())
.UseSqlServer(
"Server=(localdb)\\mssqllocaldb;TrustServerCertificate=True;Trusted_Connection=True;MultipleActiveResultSets=true;pooling=true;Command Timeout=300;");
await Task.Delay(100);
// do not dispose so that lives for the whole request
// this is a bad practice, but it is done here to demonstrate the issue
var context = new MyDbContext(options.Options);
// Use a Values statement to return customers
return Ok(context.Database.SqlQueryRaw<Customer>(
"Select 1 as Id, 'Customer 1' as Name union all Select 2 as Id, 'Customer 2' as Name union all Select 3 as Id, 'Customer 3' as Name"));
}
}
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
}
Partial workaround
If it wasn't for the $count=true I am able to partially work around this by using a custom attribute (see here for working example https://github.com/Shiney/AsyncEfCoreOdata/blob/partial-workaround/AsyncEfCoreOdata/Controllers/Controller.cs)
public class CustomEnableQueryAttribute : EnableQueryAttribute
{
public override void OnActionExecuted(ActionExecutedContext actionExecutedContext)
{
base.OnActionExecuted(actionExecutedContext);
if (actionExecutedContext.Result is ObjectResult { DeclaredType: null } objectResult)
{
if (objectResult.Value is IAsyncEnumerable<Customer>)
{
objectResult.DeclaredType = typeof(IAsyncEnumerable<Customer>);
}
}
}
}
Because this means we hit the condition to use the IAsyncEnumerable in ODataResourceSetSerializer.WriteObjectInlineAsync
However I couldn't work out a similar workaround for the count property.
Maybe the check in ODataResourceSetSerializer.WriteObjectInlineAsync should check if writeContex.Type is assignable to an IAsyncEnumerable instead of just checking the type is equal to it.
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.