Thursday, February 22, 2024

Minimal APIs vs Controller APIs: SerializerOptions.WriteIndented = true

Another thing added in ASP.NET Core / .NET 7.0 (yeah, I know it's been a while since it was released), is the "ConfigureHttpJsonOptions" extension method that lets us add JSON options to the services collection / dependency injection container. The setting that caught my eye is "WriteIndented = true". This gives a nice pretty output for JSON coming back from our APIs.

This is the difference between:

{"id":3,"givenName":"Leela","familyName":"Turanga","startDate":"1999-03-28T00:00:00-08:00","rating":8,"formatString":"{1} {0}"}

and
{
  "id": 3,
  "givenName": "Leela",
  "familyName": "Turanga",
  "startDate": "1999-03-28T00:00:00-08:00",
  "rating": 8,
  "formatString": "{1} {0}"
}
You may not want this because of the extra whitespace characters that have to come down the wire. But for the things that I work with, I want the pretty printing!

The good news is that in .NET 7.0, the new "ConfigureHttpJsonOptions" method lets us set this up (among quite a few other settings).

To use it, we just add the options to the Services in the Program.cs file of our project.


    // Set JSON indentation
    builder.Services.ConfigureHttpJsonOptions(
        options => options.SerializerOptions.WriteIndented = true);

You can check the documentation for JsonSerializerOptions Class to see what other options are available.

But there's a catch:
ConfigureHttpJsonOptions does work for Minimal APIs.
ConfigureHttpJsonOptions does not work for Controller APIs. 
Let's take a look at that. For code samples, you can check out this repository: https://github.com/jeremybytes/controller-vs-minimal-apis.

Sample Application

The sample code contains a "MinimalApi" project and a "ControllerApi" project. These were both started as fresh projects using the "dotnet new webapi" template (and the appropriate flags for minimal/controller). Both projects get their data from a 3rd project ("People.Library") that provides some hard-coded data.

Here is the minimal API that provides the data above:


    app.MapGet("/people/{id}",
        async (int id, IPeopleProvider provider) => await provider.GetPerson(id))
        .WithName("GetPerson")
        .WithOpenApi();

And here's the controller API:


    [HttpGet("{id}", Name = "GetPerson")]
    public async Task<Person?> GetPerson(
        [FromServices] IPeopleProvider provider, int id)
    {
        // This may return null
        return await provider.GetPerson(id);
    }

Both of these call the "GetPerson" method on the provider and pass in the appropriate ID.

Testing the Output
To test the output, I created a console application (the "ServiceTester" project). I did this to make sure I was not getting any automatic JSON formatting from a browser or other tool.

I created an extension method on the Uri type to make calling easier. You can find the complete code in the RawResponseString.cs file.


    public static async Task<string> GetResponse(
        this Uri uri, string endpoint)

The insides of this method make an HttpClient call and return the response.

The calling code is in the Program.cs file of the ServiceTester project.


        Uri controllerUri = new("http://localhost:5062");
        Uri minimalUri = new("http://localhost:5194");

        // Minimal API Call
        Console.WriteLine("Minimal /people/3");
        var response = await minimalUri.GetResponse("/people/3");
        Console.WriteLine(response);

        Console.WriteLine("----------");

        // Controller API Call
        Console.WriteLine("Controller /people/3");
        response = await controllerUri.GetResponse("/people/3");
        Console.WriteLine(response);

        Console.WriteLine("----------");

This creates 2 URIs (one for each API), calls "GetResponse" for each and then outputs the string to the console.

This application was also meant to show some other differences between Minimal APIs and Controller APIs. These will show up in future articles.

Without "WriteIndented = true"

Here is the output of the ServiceTester application without making any changes to the settings of the API projects.

Note that both the MinimalApi and ControllerApi services need to be running in order for the ServiceTester application to work. (I start the services in separate terminal tabs just so I can keep them running while I do my various tests.)


Minimal /people/3
{"id":3,"givenName":"Leela","familyName":"Turanga","startDate":"1999-03-28T00:00:00-08:00","rating":8,"formatString":"{1} {0}"}
----------
Controller /people/3
{"id":3,"givenName":"Leela","familyName":"Turanga","startDate":"1999-03-28T00:00:00-08:00","rating":8,"formatString":"{1} {0}"}
----------

So let's change some settings.

"WriteIndented = true"

So let's add the options setting to both projects (MinimalApi/Program.cs and ControllerApi/Program.cs):


    // Set JSON indentation
    builder.Services.ConfigureHttpJsonOptions(
        options => options.SerializerOptions.WriteIndented = true);

Then we just need to restart both services and rerun the ServiceTester application:


    Minimal /people/3
    {
      "id": 3,
      "givenName": "Leela",
      "familyName": "Turanga",
      "startDate": "1999-03-28T00:00:00-08:00",
      "rating": 8,
      "formatString": "{1} {0}"
    }
    ----------
    Controller /people/3
    {"id":3,"givenName":"Leela","familyName":"Turanga","startDate":
    "1999-03-28T00:00:00-08:00","rating":8,"formatString":"{1} {0}"}
    ----------
This shows us that the pretty formatting is applied to the Minimal API output, but not to the Controller API output.

So What's Going On?

I found out about this new setting the same way as the one in the previous article -- by hearing about it in a conference session. I immediately gave it a try and saw that it did not work on the API I was experimenting with. After few more tries, I figured out that the setting worked with the Minimal APIs that I had, but not the Controller APIs. (And I did try to get it to work with the Controller APIs in a whole bunch of different ways.)

Eventually, I came across this snippet in the documentation for the "ConfigureHttpJsonOptions" method (bold is mine):
Configures options used for reading and writing JSON when using Microsoft.AspNetCore.Http.HttpRequestJsonExtensions.ReadFromJsonAsync and Microsoft.AspNetCore.Http.HttpResponseJsonExtensions.WriteAsJsonAsync. JsonOptions uses default values from JsonSerializerDefaults.Web.
This tells us that these options are only used in certain circumstances. In this case, specifically when WriteAsJsonAsync is called.

I have not been able to find what uses "WriteAsJsonAsync" and what does not. Based on my observations, I assume that Minimal APIs do use WriteAsJsonAsync and Controller APIs do not use WriteAsJsonAsync.

So I add another item to my list of differences between Minimal APIs and Controller APIs. I guess it's time to publish this list somewhere.

Wrap Up

So, back to the beginning:
ConfigureHttpJsonOptions does work for Minimal APIs.
ConfigureHttpJsonOptions does not work for Controller APIs. 
It would be really nice if these capabilities matched or if there was a note somewhere that said "This is for minimal APIs".

Hopefully this will save you a bit of frustration in your own code. If you want to use "WriteIntented = true", then you will need to use Minimal APIs (not Controller APIs).

Happy Coding!

2 comments:

  1. Hello Jeremy, if you want to configure the JsonOptions for your controllers you need to call AddJsonOptions after AddControllers:

    builder.Services.AddControllers().AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);

    ReplyDelete
    Replies
    1. Hi Chris, Thank you for this. I appreciate the info. I will add a note to the article (and it may result in another article). Unfortunately, it makes me a bit more upset about the inconsistencies between Minimal APIs and Controller APIs. But that's something I should be used to by now.

      Delete