If you have a long running process that return array of data, or a data that you can send back as chunk of data, then you can stream the result.
Streaming from the server
In Asp.NET core 6 and later, it is so easy to stream back the result using IAsyncEnumerable
.
Let us jump into the code right away:
1 [HttpGet("ProcessLongData")]
2 [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
3 public async IAsyncEnumerable<string> ProcessLongData(string input)
4 {
5
6
7 for (var i = 0; i < 10; i++)
8 {
9 await Task.Delay(1000);
10 yield return i.ToString();
11 }
12 }
How ASP.NET serialize the result back?
Json serializer System.Text.Json
has a built-in support to serialize stream, so you don’t have to do anything because it is the default serializer for Asp.NET core.
But what in case your project is using NewtonsoftJson
?
Then you need to override the json serializer for that controller, or that method.
You do that by using the Action Filter in Asp.NET.
Here is a code that will do that:
1using System.Text.Json;
2using Microsoft.AspNetCore.Mvc;
3using Microsoft.AspNetCore.Mvc.Filters;
4using Microsoft.AspNetCore.Mvc.Formatters;
5
6public class SystemTextSerializerAttribute : ActionFilterAttribute
7{
8 public override void OnActionExecuted(ActionExecutedContext context)
9 {
10 if (context.Result is ObjectResult objectResult)
11 {
12 var options = new JsonSerializerOptions(JsonSerializerDefaults.Web);
13 objectResult.Formatters.RemoveType<NewtonsoftJsonOutputFormatter>();
14 objectResult.Formatters.Add(new SystemTextJsonOutputFormatter(options));
15 }
16 else
17 {
18 base.OnActionExecuted(context);
19 }
20 }
21}
How the browser receive the data
The browser will receive the data as chunck of data. If we inspect with chrome dev tool, we can see the following data:
.
.
.
How to handle stream in JavaScript
JavaScript has the ability to read stream using the Stream API.
A simple code will look like:
1 var postStream = async (path, body) => {
2
3 let response = await fetch(path, { method: "POST", body });
4 const reader = response.body?.getReader();
5 while (true) {
6 const { done, value } = await reader.read();
7 if (done) break;
8 if (!value) continue;
9 let textValue = new TextDecoder().decode(value);
10 console.log('the received value:', textValue);
11 }
12 }
Because the result is considered as an array, the result will be as follows:
1the received value: [1
2the received value: ,2
3the received value: ,3
4...
5the received value: ,9]
To remove the trailling brackets and commas we remove them with regex
1 let textValue = (new TextDecoder().decode(value)).replace(/^\[/, '').replace(/]$/, '').repace(/^,/, '');
How to report back to the caller:
We can use generator function feature to report to the caller:
1 var postStream = async function* (path) {
2
3 let response = await fetch(path, { method: "POST", body });
4 const reader = response.body?.getReader();
5 while (true) {
6 const { done, value } = await reader.read();
7 if (done) break;
8 if (!value) continue;
9 let textValue = (new TextDecoder().decode(value)).replace(/^\[/, '').replace(/]$/, '').repace(/^,/, '');
10 yield textValue;
11 }
12 }
And from the caller:
1 var genFunc = await postStream("path");
2 var valueObj = genFunc.next();
3 while (!valueObj.done) {
4 // do something with valueObj.value
5 valueObj = genFunc.next();
6 }
Host on Azure Web App Windows
One last thing to mention.
If you want to host your webapi application on Azure Web App (App Service) for Windows, then Azure will use IIS to host the application, and IIS has its own buffering.
To avoid IIS buffering, you add to the controller’s method the following:
1 [HttpGet("ProcessLongData")]
2 [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)]
3 public async IAsyncEnumerable<string> ProcessLongData(string input)
4 {
5
6 HttpContext.Features.Get<IHttpResponseBodyFeature>()?.DisableBuffering();
7 for (var i = 0; i < 10; i++)
8 {
9 await Task.Delay(1000);
10 yield return i.ToString();
11 }
12 }
Consume IAsyncEnumerable with HttpClient
To consume IAsyncEnumerable with an HttpClient you can do the following:
1public async Task ConsumeStreamData()
2{
3 var url = "url of stream data";
4 using var response = await _httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
5 response.EnsureSuccessStatusCode();
6 using var stream = await response.Content.ReadAsStreamAsync();
7 var dataChunks = JsonSerializer.DeserializeAsyncEnumerable<myCustomObject>(stream,
8 new JsonSerializerOptions {
9 PropertyNameCaseInsentive = true,
10 DefaultBufferSize = 200
11 });
12 await foreach (var chunk in dataChunks)
13 {
14 // do something with the data
15 }
16}
Let’s explore that code in details:
what is HttpCompleteOption.ResponseHeadersRead:
HttpClient when calling GetAsync
, PostAsync
and SendAsync
by default will start a tcp connection to the server and start stream the response and buffer it into MemoryStream
until the all data received and then it complete the method.
We can change that behavior using the overload HttpCompleteOption.ResponseHeadersRead
.
HttpCompleteOption
has two options:
- ResponseContentRead: which is the default, which is buffering the response and wait until all data end.
- ResponseHeadersRead: which return control to the caller earlier, as soon as the header received.
How to use HttpCompleteOption?
HttpCompleteOption is provided as overload parameter for GetAsync
and SendAsync
.
Let us see code with and without HttpCompletionOption:
1public async Task WithoutHttpCompletionOption()
2{
3
4 using HttpResponseMessage response = await httpClient.GetAsync("todos/3");
5
6 response.EnsureSuccessStatusCode();
7
8 var jsonResponse = await response.Content.ReadAsStringAsync();
9
10}
11
12
13public async Task WithHttpCompletionOption()
14{
15 using var response = await _httpClient.GetAsync("http://localhost:58815/books", HttpCompletionOption.ResponseHeadersRead);
16
17 response.EnsureSuccessStatusCode();
18
19 if (response.Content is object)
20 {
21 var stream = await response.Content.ReadAsStreamAsync();
22 var data = await JsonSerializer.DeserializeAsync<List<Book>>(stream);
23 // do something with the data or return it
24 }
25}