1

I want to stream an arbitrary amount of lines of plain text from an ASP.NET server to a Blazor WebAssembly client (.NET 6.0).

For testing I implemented the following dummy API:

[HttpGet("lines")]
public async IAsyncEnumerable<string> GetLines() {
    for (var i = 0; i < 10; ++i) {
        yield return "test\n";
        await Task.Delay(1000);
    }
}

On the client I tried the following approach (following these ideas):

public async IAsyncEnumerable<string?> GetLines() {
    var response = await HttpClient.GetAsync($"{apiRoot}/lines", HttpCompletionOption.ResponseHeadersRead);
    if (response.IsSuccessStatusCode) {
        var responseStream = await response.Content.ReadAsStreamAsync();
        var lines = JsonSerializer.DeserializeAsyncEnumerable<string>(responseStream);
        await foreach (var line in lines) {
            yield return line;
        }
    }
    else {
        Log.Error($"Server response code: {response.StatusCode}");
        yield return null;
    }
}

Unfortunately, instead of returning immediately, response.Content.ReadAsStreamAsync() buffers the entire stream (i.e. 10 lines of "test\n"), taking 10 s in this case, before the buffered content gets deserialized as an IAsyncEnumerable<string>.

The same behavior can be observed using HttpClient.GetStreamAsync:

public async IAsyncEnumerable<string?> GetLines() {
    var responseStream = await HttpClient.GetStreamAsync($"{apiRoot}/lines"); // buffers for 10 s
    var linesAsync = JsonSerializer.DeserializeAsyncEnumerable<string>(responseStream);
    await foreach (var line in lines) {
        yield return line;
    }
}

How can I change this so that every line sent from the server is processed immediately on the client, without any buffering? Is this an issue on the client or the server side? Is there a way to disable this buffering behavior?

Edit: After some more experimentation, I found that calling the API directly (e.g. via the browser) does indeed show the expected streaming behavior, i.e. the individual lines pop up one after the other with a 1.0 s delay. So it seems to be a client-side issue, indeed.

Frank
  • 2,738
  • 19
  • 30
  • So, you're wanting to get a line and do stuff, before getting the next? If so, then you might need to do it as separate web requests - from what I recall, you have to wait for the full response before processing it. Alternatively, it could just be that the Task.Delay on your API is the holdup. – Andrew Corrigan Nov 16 '21 at 16:53
  • Just to be sure: are you certain that the buffering is happening on the lines you highlight? – canton7 Nov 16 '21 at 17:28
  • Is your server also .NET 6? – Stephen Cleary Nov 17 '21 at 01:50
  • 1
    Also did you set `DefaultBufferSize` as noted in [the article you referenced](https://www.tpeczek.com/2021/07/aspnet-core-6-and-iasyncenumerable.html)? – Stephen Cleary Nov 17 '21 at 01:53
  • @canton7 that's what I observe using the debugger, yes. – Frank Nov 17 '21 at 10:58
  • @StephenCleary yes, everything is on .NET 6 – Frank Nov 17 '21 at 10:59
  • @StephenCleary yes, I also set the DefaultBufferSize. But that doesn't help as at this point the entire stream has already been buffered. – Frank Nov 17 '21 at 11:16
  • @AndrewCorrigan the Task.Delay is supposed to simulate that in reality, the individual lines will become available - well - with some arbitrary delay. – Frank Nov 17 '21 at 11:17

1 Answers1

2

I found a workaround that works for me, because I don't need any JSON deserialization as I want to stream raw strings.

The following implementation solves the client-side streaming issues:

public async IAsyncEnumerable<string?> GetLines() {
    using var request = new HttpRequestMessage(HttpMethod.Get, $"{apiRoot}/lines");
    request.SetBrowserResponseStreamingEnabled(true);
    var response = await HttpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);

    if (response.IsSuccessStatusCode) {
        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;
        }
    }
    else {
        Log.Error($"Server response code: {response.StatusCode}");
        yield return null;
    }
}
Frank
  • 2,738
  • 19
  • 30