How to get data as a stream from a WebAPI (.NET)
A remote web API can be consumed as a stream, which can take longer, depends an other server process, and the necessary time to generate the requested content.
It is a real scenario, if we get event, data pieces from a services, etc.
In this project I will demonstrate, how we can use a streaming API, how we can iterate through on the incoming dataset with the IAsyncEnumerable interface.
This controller generates a set of weather data, than send the dataset back to the client as a stream. Every data item serialized into Json format, and finally it sends '[DONE]' string to indicate, the streaming is over.
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild",
"Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
public async Task GetAsync()
{
Response.StatusCode = 200;
Response.ContentType = "text/html";
List<WeatherForecast> list = Enumerable.Range(1, 5)
.Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToList();
StreamWriter sw;
await using ((sw = new StreamWriter(Response.Body))
.ConfigureAwait(false))
{
foreach (WeatherForecast item in list)
{
// Thread.Sleep simulates a long running process,
// which generates some kind of output
Thread.Sleep(1000);
await sw.WriteLineAsync(item.ToString()).ConfigureAwait(false);
await sw.FlushAsync().ConfigureAwait(false);
}
await sw.WriteLineAsync("[DONE]").ConfigureAwait(false);
};
}
}
On the client side, we need a caller code, which can read and handle the incoming data. Check out the 'yield' instruction when passing every incoming data item towards the API caller. If the code detects the '[DONE]' maker, the reading ends.
public class Api
{
public async IAsyncEnumerable<string> StreamedAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default)
{
using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:7176/weatherforecast"))
{
using (HttpClient httpClient = new HttpClient())
{
//httpClient.Timeout = Timeout.InfiniteTimeSpan;
using (HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false))
{
if (response.IsSuccessStatusCode)
{
using (Stream contentStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false))
{
using (StreamReader reader = new StreamReader(contentStream))
{
string? line = null;
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null &&
!cancellationToken.IsCancellationRequested)
{
if ("[DONE]".Equals(line.Trim())) break;
if (!string.IsNullOrWhiteSpace(line))
{
yield return line;
}
}
}
}
}
else
{
string? jsonResult = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
yield return jsonResult;
}
}
}
}
}
}
Finally, a code can use the Api class to acquire the data, using IAsyncEnumerable on a proper way.
This code iterates on the incoming data asynchronously and display the given data as a text, one by one, after a data item arrived. This approch does not wait to arrive all of the data, it immediatelly displays what we got from the server.
static async Task Main(string[] args)
{
Console.WriteLine("Press a key to start");
Console.ReadKey();
Api api = new Api();
await foreach (string line in api.StreamedAsync(CancellationToken.None))
{
Console.WriteLine(line);
}
Console.WriteLine("[DONE] press a key to exit");
Console.ReadKey();
}