Using Refit with the new System.Text.Json APIs
Using Refit with the new System.Text.Json APIs in .NET Core 3.0 to boost performance
I'm a big fan of the Refit library for calling HTTP APIs from my .NET applications.
It uses code generation to let you do simple HTTP calls using interfaces and uses JSON.NET under-the-hood to handle serializing and deserializing JSON.
For example, to get a repository from the GitHub API you could define these types:
public class Organization
{
[JsonProperty("login")]
public string Login { get; set; }
[JsonProperty("id")]
public long Id { get; set; }
// ... other properties
}
[Headers("Accept: application/vnd.github.v3+json", "User-Agent: My-App/1.0.0")]
public interface IGitHub
{
[Get("/orgs/{organization}")]
Task<Organization> GetOrganizationAsync(string organization);
}
Then you could call the API like this:
var client = RestService.For<IGitHub>("https://api.github.com");
var org = await client.GetOrganizationAsync("dotnet");
This week .NET Core 3.0 preview 6 was released, and with that the new System.Text.Json APIs. These new APIs are designed to be more performant and do less allocations that JSON.NET, so should bring performance benefits to applications that use them.
So what should you do if you want to use the new System.Text.Json APIs with Refit?
Refit has some extension points to let you change how things work, including JSON (de)serialization. This means we can just provide our own IContentSerializer
implementation to the RefitSettings
class to replace JSON.NET with the new JSON APIs.
Here's the equivalent code to the above to create an IGitHub
instance with the new serializer. The complete code for the SystemTextJsonContentSerializer
class is at the bottom of this post.
var options = new JsonSerializerOptions()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
};
var settings = new RefitSettings()
{
ContentSerializer = new SystemTextJsonContentSerializer(options)
};
var client = RestService.For<IGitHub>("https://api.github.com", settings);
You'll also need to update your objects if you use attributes to control the serialization of the property names, for example the Organization
object defined above becomes the following, with [JsonProperty("...")]
replaced with [JsonPropertyName("...")]
from the System.Text.Json.Serialization
namespace.
using System.Text.Json.Serialization;
public class Organization
{
[JsonPropertyName("login")]
public string Login { get; set; }
[JsonPropertyName("id")]
public long Id { get; set; }
// ... other properties
}
I decided to put together some simple benchmarks with BenchmarkDotNet to compare the two implementations. If you want to run them yourself you can find the repo here: https://github.com/martincostello/Refit-Json-Benchmarks
There's three benchmarks that try out reading an object, reading a collection and writing an object using both JSON.NET and System.Text.Json using a stubbed-out HttpClient
to the GitHub API.
So how much faster is it?
The results are fairly impressive on my laptop running Windows 10 (full results):
Benchmark | Mean | Ratio | Allocated |
Read_Collection_NewtonsoftJson | 2.366 ms | 1.00 | 297.95 KB |
Read_Collection_SystemTextJson | 1.404 ms | 0.55 | 270.18 KB |
Read_Object_NewtonsoftJson | 219.60 μs | 1.00 | 14.15 KB |
Read_Object_SystemTextJson | 29.42 μs | 0.13 | 6.49 KB |
Write_Object_NewtonsoftJson | 22.79 μs | 1.00 | 8.09 KB |
Write_Object_SystemTextJson | 16.23 μs | 0.71 | 6.13 KB |
For these specific example requests and responses, for reading an object the mean time per operation is reduced by 87% and the amount of memory allocated reduced by 55%!
If you use Refit heavily in an existing .NET Core application to consume JSON it looks like there's a lot of performance gain to be had by switching from JSON.NET to the new System.Text.Json APIs in .NET Core 3.0!
Links
- Refit
- JSON.NET
- Try the new System.Text.Json APIs
- Announcing .NET Core 3.0 Preview 6
- Refit Benchmarks with System.Text.Json
SystemTextJsonContentSerializer
Code
using System;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using Refit;
namespace RefitWithSystemTextJson
{
public sealed class SystemTextJsonContentSerializer : IContentSerializer
{
private static readonly MediaTypeHeaderValue _jsonMediaType =
new MediaTypeHeaderValue("application/json") { CharSet = Encoding.UTF8.WebName };
public SystemTextJsonContentSerializer(JsonSerializerOptions serializerOptions)
{
SerializerOptions = serializerOptions;
}
private JsonSerializerOptions SerializerOptions { get; }
public async Task<T> DeserializeAsync<T>(HttpContent content)
{
using var utf8Json = await content.ReadAsStreamAsync();
return await JsonSerializer.ReadAsync<T>(utf8Json, SerializerOptions);
}
public async Task<HttpContent> SerializeAsync<T>(T item)
{
var stream = new MemoryStream();
try
{
await JsonSerializer.WriteAsync(item, stream, SerializerOptions);
await stream.FlushAsync();
var content = new StreamContent(stream);
content.Headers.ContentType = _jsonMediaType;
return content;
}
catch (Exception)
{
await stream.DisposeAsync();
throw;
}
}
}
}