Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support streaming results into a IAsyncEnumerable response. #3909

Open
nonwombatant opened this issue Feb 25, 2022 · 2 comments
Open

Support streaming results into a IAsyncEnumerable response. #3909

nonwombatant opened this issue Feb 25, 2022 · 2 comments

Comments

@nonwombatant
Copy link

nonwombatant commented Feb 25, 2022

Dotnet 6 now easily supports streaming IAsyncEnumerable controller responses.

It would be nice to be able to consume the stream via a generated client using JsonSerializer.DeserializeAsyncEnumerable.

Possible PR here #3908?

Edit: For anyone seeing this later, my PR doesn't and wouldn't work. In order for an IAsyncEnumerable to be memory safe the top level signature needs to be IAsyncEnumerable<TResult> not Task<IAsyncEnumerable <TResult>>. An IAsyncEnumerable uses an implicit using block, but only after iteration begins.

To work around this issue, I've been creating separate 'streaming' endpoints that get generated in nswag as functions that just return 'Stream' in addition to the normal response of IList and then using this helper method to create a IAsyncEnumerable


        private class CustomAsyncEnumerable<TData> : IAsyncEnumerable<TData>
        {
            private readonly Func<Task<Stream>> operation;
            private readonly JsonSerializerOptions jsonSerializerOptions;
            private bool didIterate;

            public CustomAsyncEnumerable(
                Func<Task<Stream>> operation,
                JsonSerializerOptions jsonSerializerOptions)
            {
                this.operation = operation ?? throw new ArgumentNullException(nameof(operation));

                this.jsonSerializerOptions = jsonSerializerOptions;
            }

            public async IAsyncEnumerator<TData> GetAsyncEnumerator(CancellationToken cancellation = default)
            {
                if (this.didIterate)
                {
                    throw new NotSupportedException($"Can only iterate once.");
                }

                this.didIterate = true;

                Stream stream;
                try
                {
                    stream = await this.operation().ConfigureAwait(false);
                }
                catch (Microsoft.Rest.HttpOperationException responseEx)
                {
                    throw new InvalidOperationException($"Error querying: {responseEx.Response?.Content}", responseEx);
                }

                using (stream)
                {
                    var rows = JsonSerializer.DeserializeAsyncEnumerable<TData>(stream, options: this.jsonSerializerOptions, cancellation);

                    await foreach (var row in rows)
                    {
                        yield return row;
                    }
                }
            }
        }
@pagefault
Copy link

pagefault commented Mar 7, 2023

This still does not work correctly at the moment.

@nonwombatant
Copy link
Author

Update, reconsidered this approach and have an updated PR #4729

Should handle the IDisposables correctly now and be memory safe

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants