Skip to content

Async Iterator Is Not Treated As IAsyncEnumerable<> #1450

@etherfactor

Description

@etherfactor

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.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions