dotnet / dotnet/aspnetcore

Minimal APIs | IResult or TypedResult specifically for IAsyncEnumerable

Open
#60,136 1 comment 6 reactions 0 assignees View on GitHub
area-minimal
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.