Minimal APIs | IResult or TypedResult specifically for IAsyncEnumerable
- Dominant language
- C#
- Stars
- 38.4k
- Forks
- 10.9k
- Avg merge
- 2d 10h
- Merged PRs (30d)
- 281
Description
### Is there an existing issue for this?
- [x] 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`.
The reason that I think it should be there is because if I return `IAsyncEnumerable` 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`.
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` 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` pattern when using a stream.
```C#
// 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(IAsyncEnumerable 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>>());
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 GetEmpty() => AsyncEnumerable.Empty();
private static async IAsyncEnumerable GetRemainingItems(IAsyncEnumerator 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), [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_
Contributor guide
Assessment
This issue has not been assessed yet.