How to configure two JSON serializers and select the correct one based on the route
Asked Answered
H

2

6

I have an ASP.NET Core Web API project using .Net Framework 4.7 and I'm upgrading to .Net Core 3.1. One of the reasons that I'm upgrading is to use the new System.Text.Json serializer.

Currently, I have some versions of the API based on the route, like:

/api/v1/controller
/api/v2/controller

And I will create a new one (v3) to use the new serializer. But here is the problem: I want to keep using JSON.Net on the older routes, to avoid any possible formating problem with the integrated clients.

Is there an easy way to configure Asp.Net Core to automatically select the correct JSON serializer based on the route?

Hortensiahorter answered 8/1, 2020 at 17:23 Comment(0)
P
11

You could create your own super InputFormatter/OutputFormatter so that it checks the condition at runtime and then make a decision to use System.Text.Json or use Newtonsoft.Json dynamically.

For example, we can check the current action method ( or controller class):

  • if it has a custom attribute of [UseSystemTextJsonAttribute], then use System.Text.Json
  • if it has a custom attribute of [UseNewtonsoftJsonAttribute], then use Newtonsoft.Json.

I create a custom InputFormatter for your reference:

// the custom attribute
internal abstract class UseJsonAttribute : Attribute, IAsyncActionFilter
{
    public Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) => next();
}
internal class UseSystemTextJsonAttribute : UseJsonAttribute { }
internal class UseNewtonsoftJsonAttribute : UseJsonAttribute { }

// Our Super Input Formatter
internal class MySuperJsonInputFormatter : TextInputFormatter
{
    public MySuperJsonInputFormatter()
    {
        SupportedEncodings.Add(UTF8EncodingWithoutBOM);
        SupportedEncodings.Add(UTF16EncodingLittleEndian);
        SupportedMediaTypes.Add("application/json");
    }

    public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
    {
        var mvcOpt= context.HttpContext.RequestServices.GetRequiredService<IOptions<MvcOptions>>().Value;
        var formatters = mvcOpt.InputFormatters;
        TextInputFormatter formatter =null; // the real formatter : SystemTextJsonInput or Newtonsoft

        Endpoint endpoint = context.HttpContext.GetEndpoint();
        if(endpoint.Metadata.GetMetadata<UseSystemTextJsonAttribute>()!= null)
        {
            formatter= formatters.OfType<SystemTextJsonInputFormatter>().FirstOrDefault();
            //formatter = formatter ?? SystemTextJsonInputFormatter
        }
        else if( endpoint.Metadata.GetMetadata<UseNewtonsoftJsonAttribute>() != null){
            // don't use `Of<NewtonsoftJsonInputFormatter>` here because there's a NewtonsoftJsonPatchInputFormatter
            formatter= (NewtonsoftJsonInputFormatter)(formatters
                .Where(f =>typeof(NewtonsoftJsonInputFormatter) == f.GetType())
                .FirstOrDefault());
        }
        else{
            throw new Exception("This formatter is only used for System.Text.Json InputFormatter or NewtonsoftJson InputFormatter");
        }
        var result = await formatter.ReadRequestBodyAsync(context,encoding);
        return result;
    }
}

internal class MySuperJsonOutputFormatter : TextOutputFormatter
{
    ... // similar to MySuperJsonInputFormatter, omitted for brevity 
}

And then configure the Json settings/options in the startup:

services.AddControllers(opts =>{ })
    .AddNewtonsoftJson(opts =>{ /**/ })
    .AddJsonOptions(opts =>{ /**/ });

Note AddNewtonsoftJson() will remove the builtin SystemTextJsonInputFormatters. So we need configure the MvcOptions manually :

services.AddOptions<MvcOptions>()
    .PostConfigure<IOptions<JsonOptions>, IOptions<MvcNewtonsoftJsonOptions>,ArrayPool<char>, ObjectPoolProvider,ILoggerFactory>((opts, jsonOpts, newtonJsonOpts, charPool, objectPoolProvider, loggerFactory )=>{
        // configure System.Text.Json formatters
        if(opts.InputFormatters.OfType<SystemTextJsonInputFormatter>().Count() ==0){
            var systemInputlogger = loggerFactory.CreateLogger<SystemTextJsonInputFormatter>();
            opts.InputFormatters.Add(new SystemTextJsonInputFormatter(jsonOpts.Value, systemInputlogger));
        }
        if(opts.OutputFormatters.OfType<SystemTextJsonOutputFormatter>().Count() ==0){
            opts.OutputFormatters.Add(new SystemTextJsonOutputFormatter(jsonOpts.Value.JsonSerializerOptions));
        }
        // configure Newtonjson formatters
        if(opts.InputFormatters.OfType<NewtonsoftJsonInputFormatter>().Count() ==0){
            var inputLogger= loggerFactory.CreateLogger<NewtonsoftJsonInputFormatter>();
            opts.InputFormatters.Add(new NewtonsoftJsonInputFormatter(
                inputLogger, newtonJsonOpts.Value.SerializerSettings, charPool, objectPoolProvider, opts, newtonJsonOpts.Value
            )); 
        }
        if(opts.OutputFormatters.OfType<NewtonsoftJsonOutputFormatter>().Count()==0){
            opts.OutputFormatters.Add(new NewtonsoftJsonOutputFormatter(newtonJsonOpts.Value.SerializerSettings, charPool, opts));
        }
        opts.InputFormatters.Insert(0, new MySuperJsonInputFormatter());
        opts.OutputFormatters.Insert(0, new MySuperJsonOutputFormatter());
    });

Now it should work fine.

Parlous answered 9/1, 2020 at 10:59 Comment(1)
Hey, thank you for this post. Its currently post-midnight my time, and this post of yours easily saved me several days of headbutting work-arounds. I had never even thought of using input/output formatter for this, particularly as they are not even mentioned on the asp.net middleware pipeline documentation. You saved me a great deal of frustration and difficulty. Thank you very, very much.Naoma
H
0

I've used this approach (create a "super" formatter which then decides which real formatter to use) to allow different routes to have different JSON formatting (camelCase or not in our scenario). This works well and allows us to use a third-party add-in which requires a specific set of formatting rules which differ from our standard...

As the third-party add-in is fully compiled we couldn't use a custom attribute on their controllers; and didn't one to add one to each of ours (!); so instead we examine the route and selected the formatter based on that.

Real life saver in this case :)

Hanker answered 25/5, 2022 at 9:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.