8000 Async Iterator Is Not Treated As IAsyncEnumerable<> · Issue #1450 · OData/AspNetCoreOData · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Async Iterator Is Not Treated As IAsyncEnumerable<> #1450

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

Open
etherfactor opened this issue Mar 26, 2025 · 8 comments · May be fixed by #1467
Open

Async Iterator Is Not Treated As IAsyncEnumerable<> #1450

etherfactor opened this issue Mar 26, 2025 · 8 comments · May be fixed by #1467
Assignees
Labels
bug Something isn't working follow-up

Comments

@etherfactor
Copy link
etherfactor commented Mar 26, 2025

Assemblies affected

  • ASP.NET Core OData 9.2.1

Describe the bug
When returning an IAsyncEnumerable from an async iterator method, it is not recognized as an IAsyncEnumerable by the ODataOutputFormatter.

Reproduce steps
Minimal reproduction of the issue.

  1. Call any of the failing requests below.
  2. The console will log the following error:
    • System.Runtime.Serialization.SerializationException: ODataResourceSetSerializer cannot write an object of type 'ODataGenericIAsyncEnumerable.Controllers.EntitiesController+<TypedAsyncEnumerableWithDelay>d__7'.
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer.WriteObjectInlineAsync(Object graph, IEdmTypeReference expectedType, ODataWriter writer, ODataSerializerContext writeContext)
         at Microsoft.AspNetCore.OData.Formatter.Serialization.ODataResourceSetSerializer.WriteObjectAsync(Object graph, Type type, ODataMessageWriter messageWriter, ODataSerializerContext writeContext)
         at Microsoft.AspNetCore.OData.Formatter.ODataOutputFormatterHelper.WriteToStreamAsync(Type type, Object value, IEdmModel model, ODataVersion version, Uri baseAddress, MediaTypeHeaderValue contentType, HttpRequest request, IHeaderDictionary requestHeaders, IODataSerializerProvider serializerProvider)
         at Microsoft.AspNetCore.OData.Formatter.ODataOutputFormatterHelper.WriteToStreamAsync(Type type, Object value, IEdmModel model, ODataVersion version, Uri baseAddress, MediaTypeHeaderValue contentType, HttpRequest request, IHeaderDictionary requestHeaders, IODataSerializerProvider serializerProvider)
         at Microsoft.AspNetCore.OData.Formatter.ODataOutputFormatter.WriteResponseBodyAsync(OutputFormatterWriteContext context, Encoding selectedEncoding)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResultFilterAsync>g__Awaited|30_0[TFilter,TFilterAsync](ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters()
      --- End of stack trace from previous location ---
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeFilterPipelineAsync>g__Awaited|20_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
         at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)
         at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|7_0(Endpoint endpoint, Task requestTask, ILogger logger)
         at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.OData.Routing.ODataRouteDebugMiddleware.Invoke(HttpContext context)
         at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
         at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
         at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
         at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
         at Microsoft.WebTools.BrowserLink.Net.BrowserLinkMiddleware.InvokeAsync(HttpContext context)
         at Microsoft.AspNetCore.Watch.BrowserRefresh.BrowserRefreshMiddleware.InvokeAsync(HttpContext context)
         at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
      

Data Model

public class Entity
{
    public int Id { get; set; }

    [Required]
    public string Name { get; set; } = null!;

    public string? Description { get; set; }
}

EDM (CSDL) Model

public class EntityConfiguration : IModelConfiguration
{
    public void Apply(
        ODataModelBuilder builder,
        ApiVersion apiVersion,
        string? routePrefix)
    {
        builder.EntitySet<Entity>("entities");
    }
}

Request/Response
The following HTTP requests were utilized. See the minimal reproduction of the issue to invoke the requests.

Expected behavior
When an IAsyncEnumerable iterator is returned, an error is not thrown, and data is streamed in the response as it becomes available from the iterator.

Screenshots
N/A

Additional context
I believe the issue stems from this line in the ODataResourceSetSerializer. When calling .GetGenericTypeDefinition() on async iterator methods, the returned type seems to almost always be a generic class that implements IAsyncEnumerable<>, not IAsyncEnumerable<> itself.

I added a line of logging (below, split for readability) to try to capture the relevant type information, and the async iterator methods have a different generic type definition than IAsyncEnumerable<>. They still implement the interface, though.

[12:03:17 INF] Attempting to return an IAsyncEnumerable of type
ODataGenericIAsyncEnumerable.Controllers.EntitiesController+<TypedAsyncEnumerableWithDelay>d__7,
with generic type
null,
and IAsyncEnumerable interface
System.Collections.Generic.IAsyncEnumerable`1[ODataGenericIAsyncEnumerable.Models.Entity]

I haven't yet been able to test and verify, but I think adjusting the following

if (writeContext.Type != null &&
    writeContext.Type.IsGenericType &&
    writeContext.Type.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>) && 
    graph is IAsyncEnumerable<object> asyncEnumerable)
{

to something akin to the following

if (writeContext.Type != null &&
    writeContext.Type.GetInterfaces().Any(i =>
        i.IsGenericType &&
        i.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) &&
    graph is IAsyncEnumerable<object> asyncEnumerable)
{

should resolve the issue.

@etherfactor etherfactor added the bug Something isn't working label Mar 26, 2025
@WanjohiSammy
Copy link
Member

@etherfactor Thank you for reporting this issue. Allow us to look into this.

However, you are welcomed to contribute.

@etherfactor
Copy link
Author

I'll see if I can get the adjustment, relevant tests, and a PR set up this weekend.

@etherfactor
Copy link
Author

I didn't have enough time to create a PR this past weekend, but I was able to at least test the proposed change, which worked as expected. Will send over a PR as soon as I have time to create one, alongside some tests.

@etherfactor
Copy link
Author

I was able to track down the issue and created #1453. The existing condition seems to work fine when returning ActionResult<IAsyncEnumerable<TEntity>>, but fails when returning IActionResult from a controller.

I added a test to cover that particular case. However, I did hit a bit of a snag that I was unable to solve. The test UsingAsAsyncEnumerableWithActionResultWorks and the new test UsingAsAsyncEnumerableWorksWithUntypedResult both hit the odata/Customers endpoint on the controller, despite invoking v2/Customers and v3/Customers via HTTP respectively. As a result, the tests do not seem to work as expected, and the ActionResult<IAsyncEnumerable<Customer>> and IActionResult actions can never be reached when debugging.

@etherfactor
Copy link
Author

Just wanted to check back. If anything else is needed on this one, please let me know. Still haven't managed to figure out why two tests keep invoking the wrong endpoint, though.

@WanjohiSammy WanjohiSammy linked a pull request Apr 18, 2025 that will close this issue
2 tasks
@WanjohiSammy
Copy link
Member

@etherfactor Would like to help review the PR #1467

@etherfactor
Copy link
Author

Sure, I can take a look. Should I delete PR #1453?

@etherfactor
Copy link
Author

Looks good to me! Feel free to delete #1453, if you want. Sorry, I wasn't sure how to link it to this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working follow-up
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants
0