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

16 June 2019 by Martin Costello |
16 June 2019 by Martin Costello

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

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;
        }
    }
}