7

I currently have a web API that

  • fetches a row of data using FromSqlRaw(...).ToListAsync() within a repository
  • returns this data as Ok(data.ToArray()) as Task<ActionResult<IEnumerable<MyClass>>> through a controller.

Now I am wondering whether I should or can use IAsyncEnumerable as a return type. The idea was to use this in the repository and the controller. However, in this (now decrepit) thread it states it should not be used. the proposed solution here would be something like:

FromSqlRaw(...).AsNoTracking().AsAsyncEnumerable()

As for the Controller I want keep the response wrapped with ActionResult to explicitly set the return code. However, that currently doesn't seem to work.

Should I just apply the solution for the repository and consume the result as a List in my controller or just keep it as it is?

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
Beltway
  • 508
  • 4
  • 17
  • I think the answer is that a consumer of the API will never receive an awaitable as result, but just results. – Gert Arnold Nov 23 '20 at 13:04
  • As for the controller it never would, as the JSON Parser is receiving the async stream and delivers it as a singular result (there was a thread on this already but I can't find it right now). I tried to get my repository to return an async stream which is consumed by the controller into a `List` using `System.linq`'s `ToListAsync()`. It works but I have no idea if this butchers the purpose of `IAsyncEnumerable` as it has to be awaited by the controller now. – Beltway Nov 24 '20 at 07:12
  • Please consider updating your answer here. NET6 introduces relevant changes so the answer marked as correct is not valid anymore. – julealgon Jul 01 '22 at 13:59

2 Answers2

10

The IAsyncEnumerable gives you an interface for pull-based asynchronous data retrieval. In other words this API represents an iterator where the next item is fetched asynchronously.

This means that you are receiving the data in several rounds and each in an asynchronous fashion.

  • Prior IAsyncEnumerable you could use IEnumerable<Task<T>>, which represents a bunch of asynchronous operations with return type T.

  • Whereas Task<IEnumerable<T>> represents a single asynchronous operation with a return type IEnumerable<T>.


Let's apply these knowledge to a WebAPI:

  • From an HTTP consumer point of view there is no difference between Task<ActionResult<T>> and ActionResult<T>. It is an implementation detail from users` perspective.
  • A WebAPI Controller's action implements a request-response model. Which means a single request is sent and a single response is received on the consumer-side.
  • If a consumer calls the same action again then a new controller will be instantiated and will process that request.

This means that the consumer of your API can't take advantage of IAsyncEnumerable if it is exposed as an action result type.

Peter Csala
  • 17,736
  • 16
  • 35
  • 75
  • But the response can be continuous, for example, when you return `PushStreamContent` content. – AgentFire Nov 24 '20 at 11:09
  • 1
    @AgentFire Yes that's true but I think it is out of scope from the question point of view. – Peter Csala Nov 24 '20 at 11:21
  • Thanks. So I have no reason to desire something like `ActionResult>` which might be the reason it's not syntactically possible atm. However, do I still gain the advantage of `IAsyncEnumerable` towards `Task>` (whatever these are) if it's consumed as a List in the controller? – Beltway Nov 25 '20 at 07:38
  • @Beltway If the following conditions are met then yes, it might make sense: 1) the individual elements will be available at different time of the future 2) you don't want to wait for all of them before you start to consume the already available one. – Peter Csala Nov 25 '20 at 09:36
  • As for 1): I am using `.AsNoTracking()` as the actuality of the data is not that important, if I understood that right it doesn't really matter. As for 2): Isn't that generally the favourable option in terms of performance and concurrency? As long as there are no disadvantages in `IAsyncEnumerable`, which I am not aware of, I would just go for it. – Beltway Nov 25 '20 at 10:28
  • @Beltway If you don't see anything against it then give it a try and measure the performance of both and then compare the results. – Peter Csala Nov 25 '20 at 11:43
6

In .net 6 IAsyncEnumerable handling for MVC was changed when using System.Text.Json:

MVC no longer buffers IAsyncEnumerable instances. Instead, MVC relies on the support that System.Text.Json added for these types.

It means that controller will start sending output immediately and a client may start process it as it receives chunks of the response.

Here is an example with help of new minimal API:

Endpoint binding:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// this endpoint return IAsyncEnumerable<TestData>
app.MapGet("/asyncEnumerable/{count}", (int count) => GetLotsOfDataAsyncEnumerable(count));

// and this one returns Task<IEnumerable<TestData>>
app.MapGet("/{count}", async (int count) => await GetLotsOfDataAsync(count));
app.Run();

Controller methods:

async Task<IEnumerable<TestData>> GetLotsOfDataAsync(int count)
{
    var list = new List<TestData>();
    for (int i = 0; i < count; i++)
    {
        await Task.Delay(10);
        list.Add(new TestData($"{i}"));
    }
    return list;
}

async IAsyncEnumerable<TestData> GetLotsOfDataAsyncEnumerable(int count)
{
    for (int i = 0; i < count; i++)
    {
        await Task.Delay(10);
        yield return new TestData($"{i}");
    }
}

class TestData
{
    public string Field { get; }

    public TestData(string field)
    {
        Field = field;
    }
}

count path variable allows to control how many data we want to retrieve in a single call.

I've tested it with curl command on a windows machine (here is the answer explaining how to measure performance with curl), results for 100 entries:

                       /100        /asyncEnumerable/100
     time_namelookup:  0.000045s   0.000034s
        time_connect:  0.000570s   0.000390s
     time_appconnect:  0.000000s   0.000000s
    time_pretransfer:  0.000648s   0.000435s
       time_redirect:  0.000000s   0.000000s
  time_starttransfer:  1.833341s   0.014880s
                       ---------------------
          time_total:  1.833411s  1.673477s

Important here to see is time_starttransfer, from curl manpage

The time, in seconds, it took from the start until the first byte was just about to be transferred. This includes time_pretransfer and also the time the server needed to calculate the result.

As you can see /asyncEnumerable endpoint started responding instantly, of course, clients of such endpoints have to be aware of such behavior to make good use of it.

Here how it looks in a cmdline: async enumerable call in curl

Monsieur Merso
  • 1,459
  • 1
  • 15
  • 18
  • 1
    Can you elaborate a little bit more on the following: _"of course, clients of such endpoints have to be aware of such behavior to make good use of it."_ Say I was consuming this from a C# client, what exactly would that client need to do to ensure he also reads as early as possible? – julealgon Jul 01 '22 at 13:20
  • 2
    In case of json, client must support streaming de-serialization of json to start processing it as soon as possible. The relatively new _System.Text.Json_ supports that. I think this [question](https://stackoverflow.com/questions/58512393/how-to-deserialize-stream-to-object-using-system-text-json-apis) addresses this problem. – Monsieur Merso Jul 01 '22 at 13:39