Streaming JSON Objects (NDJSON) With HttpClient
My first post on working with NDJSON stream in .NET was a result of a use case I needed to satisfy in one of the projects I'm involved with (and it worked great). As a result, I've received some questions on this subject. I've already answered the most common one on this blog, but there were a couple more that were really frequent. So I've decided I'm going to add a few more posts to this series:
- Fetch API, Streams API, NDJSON, and ASP.NET Core MVC
- Consuming JSON Objects Stream (NDJSON) With HttpClient
- Streaming JSON Objects (NDJSON) With HttpClient
- Receiving JSON Objects Stream (NDJSON) in ASP.NET Core MVC
Streaming NDJSON With HttpClient
When I was writing my first post about NDJSON, I didn't know there were services out there using it as an input format. The fact is that they do, which creates a need for a way to stream NDJSON with HttpClient
. The natural expectation is to be able to POST an async stream, preferably with simple and elegant code.
async IAsyncEnumerable<WeatherForecast> streamWeatherForecastsAsync()
{
for (int daysFromToday = 1; daysFromToday <= 10; daysFromToday++)
{
WeatherForecast weatherForecast = await GetWeatherForecastAsync(daysFromToday).ConfigureAwait(false);
yield return weatherForecast;
};
};
using HttpClient httpClient = new();
using HttpResponseMessage response = await httpClient.PostAsync("...",
new NdjsonAsyncEnumerableContent<WeatherForecast>(streamWeatherForecastsAsync())
).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
The way to achieve the elegancy in the above snippet is through the custom implementation of HttpContent
. The first part of it is the boilerplate - setting up media-type and charset, initializing serializer. I'm not going to dive into transcoding and limit the implementation to UTF-8.
public class NdjsonEnumerableContent<T> : HttpContent
{
private static readonly JsonSerializerOptions _defaultJsonSerializerOptions = new(JsonSerializerDefaults.Web);
private readonly IAsyncEnumerable<T> _values;
private readonly JsonSerializerOptions _jsonSerializerOptions;
public NdjsonAsyncEnumerableContent(IAsyncEnumerable<T> values, JsonSerializerOptions? jsonSerializerOptions = null)
{
_values = values ?? throw new ArgumentNullException(nameof(values));
_jsonSerializerOptions = jsonSerializerOptions ?? _defaultJsonSerializerOptions;
Headers.ContentType = new MediaTypeHeaderValue("application/x-ndjson")
{
CharSet = Encoding.UTF8.WebName
};
}
...
protected override bool TryComputeLength(out long length)
{
length = -1;
return false;
}
}
The actual work happens in SerializeToStreamAsync
. Here the goal is to enumerate the async stream, immediately serialize every incoming object and flush it over the wire together with the delimiter.
public class NdjsonEnumerableContent : HttpContent
{
private static readonly byte[] _newlineDelimiter = Encoding.UTF8.GetBytes("\n");
...
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
{
await foreach (T value in _values.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync<T>(stream, value, _jsonSerializerOptions).ConfigureAwait(false);
await stream.WriteAsync(_newlineDelimiter).ConfigureAwait(false);
await stream.FlushAsync().ConfigureAwait(false);
}
}
...
}
This way one can fully utilize async streams when working with services that can accept NDJSON. If you are looking for something ready to use, I've made a more polished version available here with some extensions to make it even more elegant to use.