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!
Hello Jeremy, if you want to configure the JsonOptions for your controllers you need to call AddJsonOptions after AddControllers:
ReplyDeletebuilder.Services.AddControllers().AddJsonOptions(options => options.JsonSerializerOptions.WriteIndented = true);
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