Part of the series: Fun with System.Text.Json
Update: PushStreamContent
& JsonContent
are now available in the Macross.Json.Extensions NuGet package.
Update (3): .NET 5 shipped with a proper version of JsonContent
so it will be removed from the next major release of Macross.Json.Extensions. That will be a breaking change for those of you using it but I don’t want to compete with the runtime. Sorry!
Most of the Microsoft documentation I have seen covers how to write services using ASP.NET Core that receive JSON requests. That is all well and good, but what if our service needs to call some other service to do its work? Have you written code like this:
public async Task SendRequestToService(HttpClient client, Uri requestUri, RequestObject request) { using StringContent stringContent = new StringContent(JsonSerializer.Serialize(request)); using HttpResponseMessage response = await client.PostAsync(requestUri, stringContent).ConfigureAwait(false); response.EnsureSuccessStatusCode(); }
Standard boilerplate for converting an object into a JSON string and POSTing to some Http endpoint.
What if I told you that pattern undoes everything System.Text.Json is doing for you?
System.Text.Json is doing its very best to be allocation free and reuse its buffers and be great at its job. How do we thank it? By allocating the result into a string! Doh.
Easy solution right? We must use a Stream of course!
public async Task SendRequestToService(HttpClient client, Uri requestUri, RequestObject request) { using MemoryStream memoryStream = new MemoryStream(); await JsonSerializer.SerializeAsync(memoryStream, request).ConfigureAwait(false); using StreamContent streamContent = new StreamContent(memoryStream); using HttpResponseMessage response = await client.PostAsync(requestUri, streamContent).ConfigureAwait(false); response.EnsureSuccessStatusCode(); }
Much better, eh? Not really. We just allocated a byte[] to temporarily store our JSON where we had a string before.
It took me a while to get around to writing this fourth part in the JSON series because this just feels wrong. Why would the library designers lead us down such an awful path? There has to be some way to do this efficiently in the library, right? Well, friends, I have looked, and I can’t find it. Please correct me if I’m wrong!
What’s interesting is there used to be a way to do this: PushStreamContent. Sorry if that link is dead, the doc has a big “deprecated” warning on it already.
That class is exactly what we need. A way to push our JSON directly to the outgoing Http request Stream once it is available.
Shall we build it?
public class PushStreamContent : HttpContent { private readonly Func<Stream, Task> _OnStreamAvailable; /// <summary> /// Initializes a new instance of the <see cref="PushStreamContent"/> class. /// </summary> /// <param name="onStreamAvailable">Callback function to write to the stream once it is available.</param> public PushStreamContent(Func<Stream, Task> onStreamAvailable) { _OnStreamAvailable = onStreamAvailable; } /// <inheritdoc/> protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) => await _OnStreamAvailable(stream).ConfigureAwait(false); /// <inheritdoc/> protected override bool TryComputeLength(out long length) { // We can't know the length of the content being pushed to the output stream. length = -1; return false; } }
And a helper for using it with JSON:
public class JsonContent<T> : PushStreamContent where T : class { private static readonly MediaTypeHeaderValue s_JsonHeader = new MediaTypeHeaderValue("application/json") { CharSet = "utf-8", }; /// <summary> /// Initializes a new instance of the <see cref="JsonContent{T}"/> class. /// </summary> /// <param name="instance">Instance to be serialized.</param> /// <param name="options"><see cref="JsonSerializerOptions"/>.</param> public JsonContent(T? instance, JsonSerializerOptions? options = null) : base(stream => JsonSerializer.SerializeAsync(stream, instance, options)) { Headers.ContentType = s_JsonHeader; } }
Now our transmission code can be written like this:
public async Task SendRequestToService(HttpClient client, Uri requestUri, RequestObject request) { using JsonContent<RequestObject> jsonContent = new JsonContent<RequestObject>(request); using HttpResponseMessage response = await client.PostAsync(requestUri, jsonContent).ConfigureAwait(false); response.EnsureSuccessStatusCode(); }
What this is essentially doing is letting System.Text.Json be awesome by writing directly to the outgoing Http request Stream and keeping the allocations to a minimum.
Update (2): In Macross.Json.Extensions 1.4.1 I changed JsonContent
to not derive from PushStreamContent
because doing that required the allocation of a delegate each time it was used. It is now slightly more efficient at the cost of some code duplication. Here’s the diff of that change.
Some data to back all of this up:
Method | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|---|
PostJsonUsingStringContent | 156.1 us | 5.33 us | 15.04 us | 149.8 us | 10.2539 | 0.9766 | – | 77.79 KB |
PostJsonUsingStreamContent | 140.1 us | 3.05 us | 6.83 us | 137.0 us | 7.5684 | 0.7324 | – | 56.03 KB |
PostJsonUsingJsonContent | 137.1 us | 1.30 us | 1.15 us | 136.8 us | 5.6152 | 0.2441 | – | 45.83 KB |
Lower allocations is better! The benchmarks are available on GitHub.