21

I encountered an interesting behavior while exploring IAsyncEnumerable in an ASP.NET Web API project. Consider the following code samples:

    // Code Sample 1
    [HttpGet]
    public async IAsyncEnumerable<int> GetAsync()
    {
        for (int i = 0; i < 10; i++)
        {
            await Task.Delay(1000);
            yield return i;
        }
    }


    // Code Sample 2
    [HttpGet]
    public async IAsyncEnumerable<string> GetAsync()
    {
        for (int i = 0; i < 10; i++)
        {
            await Task.Delay(1000);
            yield return i.ToString();
        }
    }

Sample 1 (int array) returns {} as JSON result.

Sample 2 returns expected result ["0","1","2","3","4","5","6","7","8","9"]. However, entire JSON array is returned at once after 10 seconds of wait. Shouldn't it be returned as data becomes available as expected from IAsyncEnumerable interface? Or is there any specific way this web api should be consumed?

Ravi M Patel
  • 2,905
  • 2
  • 23
  • 32
  • 7
    *Shouldn't it be returned as data becomes available as expected from IAsyncEnumerable interface?* ... and it does ... but for **json serializer** – Selvin Nov 15 '19 at 12:17
  • 1
    and Sample 1 .... seems like a bug for non reference type (if you change `IAsyncEnumerable` to `IAsyncEnumerable` - it should work but then there is a boxing involved) – Selvin Nov 15 '19 at 12:24
  • @Selvin, would you please elaborate? Or point to a code sample? I tried to consume this API with C# client, the result is same, have to wait for 10 seconds... – Ravi M Patel Nov 15 '19 at 12:46
  • *have to wait for 10 seconds* it's obviosu ... serialization is on the server side and it's not async ... elaborate about what? – Selvin Nov 15 '19 at 12:47
  • 1
    the bug is [here](https://github.com/aspnet/AspNetCore/blob/master/src/Mvc/Mvc.Core/src/Infrastructure/SystemTextJsonResultExecutor.cs#L79) ... [obviously that's why](https://dotnetfiddle.net/u4gONL) – Selvin Nov 15 '19 at 13:21
  • @Selvin - awesome find - add an answer. – tymtam Nov 15 '19 at 13:44
  • The fix is in PR as of 2021-04-09 for .NET 6.0 ... in the meantime there is a comment in the huge list of comments on that issue with a WORKAROUND - https://github.com/dotnet/runtime/issues/1570#issuecomment-676594141 – Ruskin Apr 09 '21 at 07:54

2 Answers2

16

In .NET 6, about 2 years after the question was asked, it works as you expected.

[HttpGet]
public async IAsyncEnumerable<int> Get()
{
    for(int i = 0; i < 10; i++)
    {
        await Task.Delay(TimeSpan.FromSeconds(1));
        yield return i;
    }
}

will result in the browser receiving partial results over time.

After ~3 seconds:
enter image description here

After ~5 seconds:
enter image description here

After ~10 seconds:
enter image description here


Pre .NET6

Up to .NET 6, at the time your question was asked, the web api call will not return partial json every second. It's the json serialiser who has to wait 10x1second (or the code that calls the json serialiser, which is part of ASP .NET). Once the framework code and the serialiser get all data, it will be serialised and served to the client as a single response.

In Controller action return types in ASP.NET Core web API we can read:

In ASP.NET Core 3.0 and later, returning IAsyncEnumerable from an action:

  • No longer results in synchronous iteration.
  • Becomes as efficient as returning IEnumerable.

ASP.NET Core 3.0 and later buffers the result of the following action before providing it to the serializer: (...)

tymtam
  • 31,798
  • 8
  • 86
  • 126
  • What'd happen when an exception is throw later during the enumeration after the HTTP status code 200 is sent? – TimTIM Wong Jun 10 '22 at 11:19
  • Can somebody let me know what mechanism is employed on the browser to enable such behaviour, where numbers trickle in one by one? Thank you. – Robotronx Jun 19 '23 at 16:03
7

In ASP.NET Core 5 indeed instances of the type IAsyncEnumerable have been processed by buffering the sequence in memory and formatting the buffered collection all at once. This explains, why you did not receive partial results.

However, with ASP.NET Core 6.0 this will be possible!

In ASP.NET Core 6, when formatting using System.Text.Json, MVC no longer buffers IAsyncEnumerable instances. Instead, MVC relies on the support that System.Text.Json added for these types (reference)

The release for ASP.NET Core 6 is planned for November 2021 (reference). It is already possible to test the new behavior using preview releases. I successfully tested the following code using the preview release 6.0.100-preview.6.21355.2. The code produces an infinite stream of integers and returns it via a controller using IAsyncEnumerable. The while (true) loop "proves" that data is returned before everything is processed because obviously the loop will never terminate*.

using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;

namespace dot_net_api_streaming.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class LoopController : ControllerBase
    {

        [HttpGet]
        public IAsyncEnumerable<int> Get()
        {
            return GetInfiniteInts();
        }

        private async IAsyncEnumerable<int> GetInfiniteInts()
        {
            int index = 0;
            while (true)
                yield return index++;
        }
    }
}
 

*Please keep that in mind when experimenting with my code so your machine will not crash :)

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Fable
  • 371
  • 1
  • 4
  • 11
  • @RaviMPatel I reverted this question to the 1st revision, because I think that your contribution deviates enough from the original answer to be considered a different answer. You could consider posting your contribution ([revision 3](https://stackoverflow.com/revisions/68768015/3)) as a new answer, so that that it can be evaluated (voted) independently. – Theodor Zoulias Feb 13 '22 at 11:17
  • If an error happens in the middle of iteration how the HttpStatus will be handled? There was a question posted as a comment on the earlier answer. – Arjun Menon Oct 08 '22 at 19:48