-
Notifications
You must be signed in to change notification settings - Fork 180
Description
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.
- Call any of the failing requests below.
- 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.
- GET https://localhost:7162/api/v1/entities?variant=None
- Succeeds (but processes as an
IEnumerable, not anIAsyncEnumerable)
- Succeeds (but processes as an
- GET https://localhost:7162/api/v1/entities?variant=Typed
- Fails
- GET https://localhost:7162/api/v1/entities?variant=Generic
- Fails
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.