-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Description
Is there an existing issue for this?
- I have searched the existing issues
Is your feature request related to a problem? Please describe the problem.
It cannot be that I am the only one to ever think of this, so there must a good reason why there is no member of the Results
or TypedResults
class specifically designed for IAsyncEnumerable<T>
.
The reason that I think it should be there is because if I return IAsyncEnumerable<T>
directly from my endpoint, then that makes it difficult to also be able to return Results.Problem()
(for FluentValidation) or Result.Status()
if an error occurs before the stream. That's of course because the return type cannot be inferred, because one is IResult
and the other is IAsyncEnumerable<T>
.
I did build my own IResult to get around this, and in the process of doing so, my throughput jumped dramatically. I do not know for certain why, but I speculate that it may have something to do with the fact that I am using a JsonSerializerContext
and source generated (de)serialization code, but unbeknownst to me, even though the type I was returning was decorated with the JsonSerializable
attribute, what actually needed to be decorated was my type being wrapped with IAsyncEnumerable.
Describe the solution you'd like
All I am asking is that there be method, call it what you will, or at least give an overload, on TypedResults
and/or Results
specifically for IAsyncEnumerable
Here's the code I wrote. If anything, I hope this helps someone else out who is running into the same problem.
The ServiceResultAccessor
is just a type I created that is similar in concept to IHttpContextAccessor
in that it uses AsyncLocal<T>
for storing, not HttpContext, but a result status enum. So if there is any error on the backend pulling the data that I am sending out, the status will be written to it, then when I await the first entry in the IAsyncEnumerable, I am able to see if a backend error occurs and update the response status code to reflect that. This provides me a workaround for not being able to use the Result<T>
pattern when using a stream.
// ReSharper disable StaticMemberInGenericType
using System.Reflection;
using System.Runtime.CompilerServices;
using API.Logging;
using Microsoft.AspNetCore.Http.Metadata;
using Services.ApiClients.Infrastructure;
using static API.Serialization.ApiJsonContext;
using static Microsoft.AspNetCore.Http.StatusCodes;
namespace API.ApiResults;
public sealed class AsyncEnumerableResult<T>(IAsyncEnumerable<T> stream, ServiceResultStatusAccessor accessor, CancellationToken cancellationToken): IEndpointMetadataProvider, IResult
{
private static readonly JsonSerializerOptions JsonOptions = Default.Options;
private const string JsonContentType = "application/json; charset=utf-8";
public async Task ExecuteAsync(HttpContext httpCtx)
{
var (response, logger) = (httpCtx.Response, httpCtx.RequestServices.GetService<ILogger<AsyncEnumerableResult<T>>>());
try
{
await using var enumerator = stream.GetAsyncEnumerator(cancellationToken);
switch (await enumerator.MoveNextAsync(cancellationToken))
{
// some problem occurred in the processing, write the status code and bail
case true or false when accessor.IsResultFailed(out var status):
if(!response.HasStarted) response.StatusCode = status.ToHttpStatusCodeInt();
logger?.AsyncEnumerableErrorStatusCode(response.StatusCode);
break;
case false:
if (!response.HasStarted) response.StatusCode = Status200OK;
await response.WriteAsJsonAsync(GetEmpty(), JsonOptions, cancellationToken);
break;
case true:
if (!response.HasStarted) response.StatusCode = Status200OK;
await response.WriteAsJsonAsync(GetRemainingItems(enumerator, cancellationToken), JsonOptions, cancellationToken);
break;
}
}
catch (Exception ex)
{
logger?.AsyncEnumerableResultException(typeof(T).Name, ex);
}
}
private static IAsyncEnumerable<T> GetEmpty() => AsyncEnumerable.Empty<T>();
private static async IAsyncEnumerable<T> GetRemainingItems(IAsyncEnumerator<T> enumerator, [EnumeratorCancellation] CancellationToken ct)
{
yield return enumerator.Current;
while (await enumerator.MoveNextAsync(ct)) yield return enumerator.Current;
}
static void IEndpointMetadataProvider.PopulateMetadata(MethodInfo method, EndpointBuilder builder)
{
builder.Metadata.Add(new ProducesResponseTypeMetadata(Status200OK, typeof(IAsyncEnumerable<T>), [JsonContentType]));
builder.Metadata.Add(new ProducesResponseTypeMetadata(Status400BadRequest));
builder.Metadata.Add(new ProducesResponseTypeMetadata(Status401Unauthorized));
builder.Metadata.Add(new ProducesResponseTypeMetadata(Status404NotFound));
builder.Metadata.Add(new ProducesResponseTypeMetadata(Status500InternalServerError));
builder.Build();
}
}
Additional context
No response