How can I "stream" search results from a minimal API using IAsyncEnumerable in c# using .NET 6?
Asked Answered
D

0

8

Overview

We're moving our database access to API calls and I need to return search results from a database as they're being found using that API. The plan was to use IAsyncEnumerable, but I'm having trouble dealing with the HttpClient buffer (at least I think). The call is being triggered by a button click on the client side in WPF that sends the search term to a client side API service. That service makes the API request which then uses a repository to do the work. I'm using a minimal API and both the client and API are using .Net 6.

Problem

The problem I'm having is that, while the IAsyncEnumerable part works on the API side by yield returning each entry one at a time, the entire result still gets passed back as a single response instead of yielding back one response at a time.

Info & Questions

At this point I've simplified it down just to try and get the concept to work. The IAsyncEnumerable part works just fine within the client ApiService itself when it's not using HttpClient, but as soon as that is introduced I start running into issues. You can see the various solutions I've tried in the Regions of the GetTestStreamAsync method.

So I guess one of the main questions is, can IAsyncEnumerable even do this? And if not, what's the solution?

Client-side Code

MainWindow.xaml.cs

private async void btnSearch_Click(object sender, RoutedEventArgs e)
{
    // This works
    //var result = apiService.GetTestAsync();
    // This doesn't
    var result = apiService.GetTestStreamAsync(txtSearchTerms.Text);

    await foreach(var item in result!)
    {
        lstResults.Items.Add(item);
    }
}

MainWindow.xaml

<Grid>
    <TextBox x:Name="txtSearchTerms"
                HorizontalAlignment="Left"
                Margin="86,92,0,0"
                TextWrapping="Wrap"
                VerticalAlignment="Top"
                Width="340"
                TabIndex="1" />

    <Button x:Name="btnSearch"
            Content="Search"
            HorizontalAlignment="Left"
            Margin="387,115,0,0"
            VerticalAlignment="Top"
            TabIndex="2"
            Click="btnSearch_Click" />

    <ListBox x:Name="lstResults"
                HorizontalAlignment="Left"
                Margin="86,140,0,0"
                VerticalAlignment="Top"
                Height="204"
                Width="340"
                Background="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"
                Padding="10,10,10,10"
                ScrollViewer.CanContentScroll="True"
                ScrollViewer.VerticalScrollBarVisibility="Auto" />

</Grid>

ApiServics.cs

public class ApiService : IApiService
{
    // This works
    public async IAsyncEnumerable<int> GetTestAsync()
    {
        for (int i = 1; i < 6; i++)
        {
            yield return i;
            await Task.Delay(250);
        }
    }

    // This doesn't
    public async IAsyncEnumerable<string> GetTestStreamAsync(string input)
    {
        HttpClient httpClient = new() { BaseAddress = new Uri($"https://localhost:7203/test/{input}") };


        #region nope 6 (but seems closer as it hits the await foreach and lists individually in xaml listbox, but still waits for all results)
        Stream? response = await httpClient.GetStreamAsync(httpClient.BaseAddress);
        await foreach (string? item in System.Text.Json.JsonSerializer.DeserializeAsyncEnumerable<string>(response))
        {
            yield return item!;
        }
        #endregion


        #region nope 8 (from https://github.com/dotnet/aspnetcore/issues/12883)
        //using Stream? stream = await httpClient.GetStreamAsync(httpClient.BaseAddress);
        //using StreamReader? reader = new(stream);
        //using JsonTextReader? jsonReader = new(reader);

        //Newtonsoft.Json.JsonSerializer serializer = new();
        //while (await jsonReader.ReadAsync())
        //{
        //    var result = serializer.Deserialize<object>(jsonReader);
        //    yield return result?.ToString()!;
        //}
        #endregion


        #region nope 7 (from https://mcmap.net/q/589795/-streaming-lines-of-text-over-http-with-blazor-using-iasyncenumerable)
        //using var request = new HttpRequestMessage(HttpMethod.Get, httpClient.BaseAddress);
        //request.SetBrowserResponseStreamingEnabled(true);
        //var response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

        //response.EnsureSuccessStatusCode();

        //using var responseStream = await response.Content.ReadAsStreamAsync();
        //using var reader = new StreamReader(responseStream);
        //string? line = null;
        //while ((line = await reader.ReadLineAsync()) != null)
        //{
        //    yield return line;
        //}
        #endregion


        #region nope 5
        //using Stream response = await httpClient.GetStreamAsync(httpClient.BaseAddress);
        //using StreamReader reader = new StreamReader(response);
        //while (!reader.EndOfStream)
        //{
        //    string? line = await reader.ReadLineAsync();
        //    yield return line!;
        //}
        #endregion


        #region nope 4
        //HttpResponseMessage response = await httpClient.GetAsync(httpClient.BaseAddress);
        //response.EnsureSuccessStatusCode();
        //var stream = await response.Content.ReadAsStreamAsync();
        //await foreach (string? item in JsonSerializer.DeserializeAsyncEnumerable<string>(stream))
        //{
        //    yield return item!;
        //}
        #endregion


        #region nope 3
        //HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, httpClient.BaseAddress);
        //request.SetBrowserResponseStreamingEnabled(true);

        //using HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
        //response.EnsureSuccessStatusCode();

        //using Stream responseStream = await response.Content.ReadAsStreamAsync();

        //await foreach (string? item in JsonSerializer.DeserializeAsyncEnumerable<string>(responseStream))
        //{
        //    yield return item!;
        //}
        #endregion


        #region nope 2
        //var response = await httpClient.GetStringAsync(httpClient.BaseAddress);
        //var responseStr = JsonSerializer.Deserialize<string>(response);
        //yield return responseStr!;
        #endregion


        #region nope 1
        //HttpResponseMessage response = await httpClient.GetAsync(httpClient.BaseAddress);
        //response.EnsureSuccessStatusCode();
        //yield return await response.Content.ReadAsStringAsync();
        #endregion
    }
}

IApiService.cs

public interface IApiService
{
    public IAsyncEnumerable<int> GetTestAsync();
    public IAsyncEnumerable<string> GetTestStreamAsync(string input);
}

API Code

Program.cs

app.MapGet("/test/{stringInput}", IAsyncEnumerable<string> (string stringInput, IDataRepository rep) =>
{
    return TestStreamAsync();

    async IAsyncEnumerable<string> TestStreamAsync()
    {
        IAsyncEnumerable<string> results = rep.TestStreamAsync(stringInput);

        await foreach (string item in results)
        {
            yield return item;
        }
    }
}).WithName("TestStream");

DataRepository.cs

internal class DataRepository : IDataRepository
{
    public async IAsyncEnumerable<string> TestStreamAsync(string input)
    {
        for (int i = 1; i < 6; i++)
        {
            yield return $"{input}-{i}";
            await Task.Delay(250);
        }
    }
}

IDataRepository.cs

internal interface IDataRepository
{
    IAsyncEnumerable<string> TestStreamAsync(string input);
}
Diffractive answered 2/4, 2022 at 15:6 Comment(1)
Did you figure it out eventually?Efik

© 2022 - 2024 — McMap. All rights reserved.