posts

Inject a service into a System.Text.Json converter

Most JSON converters are fairly simple, and typically self-contained. But once in a while, you need to do something a little more complex in a converter, and you end up needing to call a service. However, there’s no built-in dependency injection in System.Text.Json converters… How can you access the service you need?

There are basically two variants of this problem. One has a simple solution, the other is a bit of a hack…

Global converter

What I mean by “global converter” is a converter that applies to all instances of the type(s) it supports. You just add it to JsonSerializerOptions.Converters, and it handles the conversion of any value of the supported type(s).

In this case, it’s pretty simple: create the converter manually by passing the service it depends on to the constructor, and add it to the JsonSerializerOptions:

var options = new JsonSerializerOptions
{
    Converters =
    {
        new FooConverter(fooService)
    }
};

This works fine when you’re explicitly serializing something, but typically in an ASP.NET Core app, JSON serialization is done automatically by the MVC framework. You configure the JSON serialization with the AddJsonOptions method, where you don’t have access to the services, because the service provider isn’t built yet. In this case, you can register a class that will configure the options, like this:


services.ConfigureOptions<ConfigureJsonOptions>();

...

private class ConfigureJsonOptions : IConfigureOptions<JsonOptions>
{
    private readonly IFooService _fooService;

    public ConfigureJsonOptions(IFooService fooService)
    {
        _fooService = fooService;
    }

    public void Configure(JsonOptions options)
    {
        options.JsonSerializerOptions.Converters
            .Add(new FooConverter(_fooService));
    }
}

Case-by-case converter

What I mean by that is a converter that you apply on a case by case basis, by adding a JsonConverter attribute to properties that need to use the converter. For instance:

[JsonConverter(typeof(FooConverter))]
public Foo Foo { get; set; }

In this situation, you have no control over how the converter is instantiated, so you can’t inject a service in the constructor. The situation looks hopeless… Time to cheat!

The Read and Write methods of a JsonConverter have the JsonSerializerOptions as a parameter. Maybe we can use this? I half expected to find a ServiceProvider property or something similar on that object, but unfortunately, there isn’t one. Maybe we could hijack one of the other properties? Most of them are primitive types or enums; PropertyNamingPolicy and Encoder are probably not very good candidates. That leaves Converters: we could add a “dummy” converter, that doesn’t actually convert anything, but exposes a ServiceProvider. We could then retrieve it from the options and use it to resolve the service we need. Let’s do this!

First, the dummy converter itself. We could just inject a IServiceProvider into it, but it would be the root provider, so we wouldn’t be able to resolve scoped services. We could, instead, inject a IHttpContextAccessor, from which we’ll be able to access the IServiceProvider for the current request. But then we would only be able to resolve services in the context of handling a HTTP request. So let’s combine both approaches:

/// <summary>
/// This isn't a real converter. It only exists as a hack to expose
/// IServiceProvider on the JsonSerializerOptions.
/// </summary>
public class ServiceProviderDummyConverter :
    JsonConverter<object>,
    IServiceProvider
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IServiceProvider _serviceProvider;

    public ServiceProviderDummyConverter(
        IHttpContextAccessor httpContextAccessor,
        IServiceProvider serviceProvider)
    {
        _httpContextAccessor = httpContextAccessor;
        _serviceProvider = serviceProvider;
    }

    public object GetService(Type serviceType)
    {
        // Use the request services, if available, to be able to resolve
        // scoped services.
        // If there isn't a current HttpContext, just use the root service
        // provider.
        var services = _httpContextAccessor.HttpContext?.RequestServices
            ?? _serviceProvider;
        return services.GetService(serviceType);
    }

    public override bool CanConvert(Type typeToConvert) => false;

    public override object Read(
        ref Utf8JsonReader reader,
        Type typeToConvert,
        JsonSerializerOptions options)
    {
        throw new NotSupportedException();
    }

    public override void Write(
        Utf8JsonWriter writer,
        object value,
        JsonSerializerOptions options)
    {
        throw new NotSupportedException();
    }
}

Note: CanConvert always returns false, and the Read and Write methods always throw: this converter can’t actually convert anything, it only exists to expose a service provider.

Then, let’s add this converter to the JSON options, using IConfigureOptions. Note that we also need to register the IHttpContextAccessor, which isn’t registered by default:

services.AddHttpContextAccessor();
services.ConfigureOptions<ConfigureJsonOptions>();

...

private class ConfigureJsonOptions : IConfigureOptions<JsonOptions>
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly IServiceProvider _serviceProvider;

    public ConfigureJsonOptions(
        IHttpContextAccessor httpContextAccessor,
        IServiceProvider serviceProvider)
    {
        _httpContextAccessor = httpContextAccessor;
        _serviceProvider = serviceProvider;
    }

    public void Configure(JsonOptions options)
    {
        options.JsonSerializerOptions.Converters.Add(
            new ServiceProviderDummyConverter(
                _httpContextAccessor,
                _serviceProvider));
    }
}

Finally, let’s write an extension method to easily retrieve the IServiceProvider from the JSON options:

public static IServiceProvider GetServiceProvider(
    this JsonSerializerOptions options)
{
    return options.Converters.OfType<IServiceProvider>().FirstOrDefault()
        ?? throw new InvalidOperationException(
            "No service provider found in JSON converters");
}

We now have everything we need. In our real JSON converter, we can now retrieve the service we need like this:

public override object Read(
    ref Utf8JsonReader reader,
    Type typeToConvert,
    JsonSerializerOptions options)
{
    var fooService = options.GetServiceProvider()
        .GetRequiredService<IFooService>();
    // Do something with the service...
}

Well, that’s an ugly hack! But it serves its purpose, and until there’s an official solution to the problem, I can’t think of a better workaround…