19

I've been playing around with Blazor and the IAsyncEnumerable feature in C# 8.0. Is it possible to use IAsyncEnumerable and await within Razor Pages to progressively display markup with data?

Example service:

private static readonly string[] games = new[] { "Call of Duty", "Legend of Zelda", "Super Mario 64" };
public async IAsyncEnumerable<string> GetGames()
{
   foreach (var game in games)
   {
     await Task.Delay(1000);
     yield return game;
   }
}

Example in razor page:

@await foreach(var game in GameService.GetGames())
{
  <p>@game</p>
}

This gives error CS4033: The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'.

Any ideas if this is possible?

VirtualValentin
  • 1,131
  • 1
  • 11
  • 19
  • Why? With Razor Pages the code will still run on the server before sending the generated HTML to the client. With server-side Blazor, modifications to the UI are sent to the client using SignalR. With client side it's the *client* that asks for the data from the server – Panagiotis Kanavos Sep 10 '19 at 13:02
  • 1
    I can't see anything wrong here, so I can only assume there's some language incompatibility. As you sure you're using C# 8 in the project? Are you using the latest preview release of Visual Studio? Aside from that, Razor may not have full support for this yet. I can't find anything one way or another as to whether you can use `await foreach` in Razor. FWIW, this won't return the response in chunks, regardless. It's just going to (in principle at least) more efficiently utilize memory resources, but you're still not going to get the response until everything is done. – Chris Pratt Sep 10 '19 at 13:03
  • 1
    `IAsyncEnumerable` only allows you to stream the response when it's returned directly as the response, not as part of some operation in creating that response. – Chris Pratt Sep 10 '19 at 13:04
  • 1
    I suspect what you're looking for is [SignalR's streaming](https://learn.microsoft.com/en-us/aspnet/core/signalr/streaming?view=aspnetcore-2.2). This allows you to generate events on the server asynchronously and sends them to the client. You *can* use `await foreach` on the client to listen to the ChannelReader's `ReadAllAsync` method and update the UI. `ReadAllAsync` is a simple iterator method that returns an `IAsyncEnumerable` with the reader's results – Panagiotis Kanavos Sep 10 '19 at 13:04
  • @ChrisPratt: Yes, I'm using C# 8.0 and the latest preview of Visual Studio. I'm assuming Razor doesn't support this since there isn't really a need for it, as both you and PanagiotisKanavos mentioned. Yes, I'm aware that SignalR can do that, I'm just trying to learn the scope of what IAsyncEnumerable can and cannot do. – VirtualValentin Sep 10 '19 at 13:22
  • @VirtualValentino wait a bit, as *server* side Blazor would probably work. [This YouTube video](https://www.youtube.com/watch?v=VD6DGuY5d6k) shows code that changed the ForecastService in the BlazorServer template to return an IAsyncEnumerable. On the *client* you could display the data dynamically if you used differnt code to add elements to a div instead of emiting text directly – Panagiotis Kanavos Sep 10 '19 at 13:28
  • 1
    @VirtualValentino it works. You could use a `List<>` to store incoming services and leave the rendering code more or less as is – Panagiotis Kanavos Sep 10 '19 at 13:52

2 Answers2

14

Server-side Razor allows what you describe. This video describes the code in this Github repo that shows how to use IAsyncEnumerable by modifying the ForecastService example in server-side Blazor template.

Modifying the service itself is easy, and actually results in cleaner code :

    public async IAsyncEnumerable<WeatherForecast> GetForecastAsync(DateTime startDate)
    {
        var rng = new Random();
        for(int i=0;i<5;i++)
        {
            await Task.Delay(200);
            yield return new WeatherForecast
            {
                Date = startDate.AddDays(i),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            };
        }
    }

The Blazor page on the other hand is more complicated. It's not just that the loop would have to finish before the HTML was displayed, you can't use await foreach in the page itself because it's not asynchronous. You can only define asynchronous methods in the code block.

What you can do, is enumerate the IAsyncEnumerable and notify the page to refresh itself after each change.

The rendering code itself doesn't need to change :

    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in forecasts)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>

OnInitializedAsync needs to call StateHasChanged() after receiving each item :

    List<WeatherForecast> forecasts;

    protected override async Task OnInitializedAsync()
    {
        forecasts =new List<WeatherForecast>(); 
        await foreach(var forecast in ForecastService.GetForecastAsync(DateTime.Now))
        {
            forecasts.Add(forecast);
            this.StateHasChanged();            
        }
    }

In the question's example, incoming games could be stored in a List, leaving the rendering code unchanged :

@foreach(var game in games)
{
  <p>@game</p>
}

@code {
    List<string> games;

    protected override async Task OnInitializedAsync()
    {
        games =new List<games>(); 
        await foreach(var game in GameService.GetGamesAsync())
        {
            games.Add(game);
            this.StateHasChanged();            
        }
    }
}
juFo
  • 17,849
  • 10
  • 105
  • 142
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Ahh, gotcha. Yes, I was trying to follow the Weather example for this as well, and I got it to work by saving it as a list, but it was just displaying everything all at once. Refreshing seemed to get at what I was going for. – VirtualValentin Sep 12 '19 at 10:41
  • That is just awesome! – Sean Kearon Dec 11 '19 at 17:52
5

You can't write await foreach on .razor template code. But, as workaround, you can write it at @code section:

@if (@gamesUI == null)
{
    <p><em>Loading...</em></p>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Game</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var game in gamesUI)  // <--- workaround
            {
                <tr>
                    <td>@game</td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    List<string> gamesUI;  // <--- workaround

    protected override async Task OnInitializedAsync()
    {
        gamesUI = new List<string>();
        await foreach(var game in GService.GetgamesAsync() )
        {
            gamesUI.Add(game);
            this.StateHasChanged();
        }
    }
}

Effect:

enter image description here

Yielding data:

        private static readonly string[] games = new[] { "Call of Duty", "Legend of Zelda", "Super Mario 64", "Bag man" };


        public async IAsyncEnumerable<string> GetgamesAsync()
        {
            var rng = new Random();

            foreach (var game in games)
            {
                await Task.Delay(1000);
                yield return game;
            }
        }
dani herrera
  • 48,760
  • 8
  • 117
  • 177
  • In server-side Blazor, awaiting on the `IAsyncEnumerable` will cause your pages not to load - you have to run it as a detatched `Task` in the background – Aaronontheweb Nov 30 '22 at 18:12