diff --git a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Controllers/ProductController.cs b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Controllers/ProductController.cs index 6ce51f034a..188b7693d2 100644 --- a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Controllers/ProductController.cs +++ b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Controllers/ProductController.cs @@ -1,5 +1,4 @@ using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json; namespace GlobalDefaultJsonSerializationoptions.Controllers; @@ -16,6 +15,6 @@ public ActionResult CreateProduct(Product product) [HttpPost("save")] public ActionResult SaveProduct(Product product) { - return Ok(JsonConvert.SerializeObject(product)); + return Ok(product); } } diff --git a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions.csproj b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions.csproj index c7cc13203b..703f2e6bb7 100644 --- a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions.csproj +++ b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions.csproj @@ -1,15 +1,15 @@ - net8.0 + net10.0 enable enable true - - + + diff --git a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Program.cs b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Program.cs index 0397cc9e1d..adc6baf8a5 100644 --- a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Program.cs +++ b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptions/Program.cs @@ -1,27 +1,22 @@ -using Microsoft.AspNetCore.Mvc; +using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; +using GlobalDefaultJsonSerializationoptions; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; -using GlobalDefaultJsonSerializationoptions; -using System.Text.Encodings.Web; var builder = WebApplication.CreateBuilder(args); -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(); -builder.Services.AddControllers(); - -builder.Services.Configure(options => -{ - options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; - options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; - options.JsonSerializerOptions.WriteIndented = false; - options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Default; - options.JsonSerializerOptions.AllowTrailingCommas = true; - options.JsonSerializerOptions.MaxDepth = 3; - options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; -}); +var controllers = builder.Services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; + options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; + options.JsonSerializerOptions.WriteIndented = false; + options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Default; + options.JsonSerializerOptions.AllowTrailingCommas = true; + options.JsonSerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; + }); builder.Services.ConfigureHttpJsonOptions(options => { @@ -30,28 +25,28 @@ options.SerializerOptions.WriteIndented = false; options.SerializerOptions.Encoder = JavaScriptEncoder.Default; options.SerializerOptions.AllowTrailingCommas = true; - options.SerializerOptions.MaxDepth = 3; options.SerializerOptions.NumberHandling = JsonNumberHandling.AllowReadingFromString; }); -JsonConvert.DefaultSettings = () => new JsonSerializerSettings +// AddNewtonsoftJson() replaces both MVC formatters, so it would take the +// System.Text.Json configuration above out of play for every controller action. +// The sample demonstrates all three approaches, so this one is registered behind +// a configuration switch: run with UseNewtonsoftJson=true to serialize controller +// responses with Json.NET instead. +if (builder.Configuration.GetValue("UseNewtonsoftJson")) { - ContractResolver = new CamelCasePropertyNamesContractResolver(), - Formatting = Formatting.Indented, - NullValueHandling = NullValueHandling.Ignore, - DateFormatString = "dd-MM-yyyy", - DefaultValueHandling = DefaultValueHandling.Ignore, - MaxDepth = 3 -}; + controllers.AddNewtonsoftJson(options => + { + options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + options.SerializerSettings.Formatting = Formatting.Indented; + options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + options.SerializerSettings.DateFormatString = "dd-MM-yyyy"; + options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Ignore; + }); +} var app = builder.Build(); -if (app.Environment.IsDevelopment()) -{ - app.UseSwagger(); - app.UseSwaggerUI(); -} - app.UseHttpsRedirection(); app.UseAuthorization(); @@ -64,3 +59,5 @@ }); app.Run(); + +public partial class Program; diff --git a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/GlobalDefaultJsonSerializationoptionsUnitTests.csproj b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/GlobalDefaultJsonSerializationoptionsUnitTests.csproj index f0985e8813..4522c9d726 100644 --- a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/GlobalDefaultJsonSerializationoptionsUnitTests.csproj +++ b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/GlobalDefaultJsonSerializationoptionsUnitTests.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 enable enable @@ -10,11 +10,11 @@ - - - - - + + + + + diff --git a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/GlobalJsonOptionsIntegrationTests.cs b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/GlobalJsonOptionsIntegrationTests.cs new file mode 100644 index 0000000000..3ded8c4eeb --- /dev/null +++ b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/GlobalJsonOptionsIntegrationTests.cs @@ -0,0 +1,90 @@ +using System.Text; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace GlobalDefaultJsonSerializationoptionsUnitTests; + +[TestClass] +public class GlobalJsonOptionsIntegrationTests +{ + private const string RequestBody = """ + { + "Id": 1, + "Name": null, + "Price": 0, + "Quantity": "5", + "ReleaseDate": "2024-04-14T10:49:31.813Z", + "Manufacturer": { "Name": "Apple", "Location": "California" } + } + """; + + private static HttpContent JsonContent() => + new StringContent(RequestBody, Encoding.UTF8, "application/json"); + + private static WebApplicationFactory CreateFactory(bool useNewtonsoftJson) => + new WebApplicationFactory() + .WithWebHostBuilder(builder => + builder.UseSetting("UseNewtonsoftJson", useNewtonsoftJson.ToString())); + + private static async Task PostAsync(HttpClient client, string route) + { + var response = await client.PostAsync(route, JsonContent()); + var body = await response.Content.ReadAsStringAsync(); + + Assert.IsTrue(response.IsSuccessStatusCode, body); + + return JsonNode.Parse(body)!.AsObject(); + } + + [TestMethod] + public async Task GivenSystemTextJsonOptions_WhenControllerActionResponds_ThenItAppliesTheConfiguredOptions() + { + using var factory = CreateFactory(useNewtonsoftJson: false); + using var client = factory.CreateClient(); + + var payload = await PostAsync(client, "/api/Product"); + + // Camel casing, nulls dropped, and a number read from a string. + CollectionAssert.AreEquivalent( + new[] { "id", "price", "quantity", "releaseDate", "manufacturer" }, + payload.Select(property => property.Key).ToArray()); + Assert.AreEqual(5, (int)payload["quantity"]!); + Assert.AreEqual("Apple", (string?)payload["manufacturer"]!["name"]); + + // The Newtonsoft date format is not registered on this run. + Assert.AreNotEqual("14-04-2024", (string?)payload["releaseDate"]); + } + + [TestMethod] + public async Task GivenNewtonsoftJsonIsRegistered_WhenControllerActionResponds_ThenJsonNetWritesTheResponse() + { + using var factory = CreateFactory(useNewtonsoftJson: true); + using var client = factory.CreateClient(); + + var payload = await PostAsync(client, "/api/Product/save"); + + // AddNewtonsoftJson() replaced the MVC formatters: Json.NET's date format + // and DefaultValueHandling.Ignore are both visible in the response. + CollectionAssert.AreEquivalent( + new[] { "id", "quantity", "releaseDate", "manufacturer" }, + payload.Select(property => property.Key).ToArray()); + Assert.AreEqual("14-04-2024", (string?)payload["releaseDate"]); + } + + [TestMethod] + public async Task GivenNewtonsoftJsonIsRegistered_WhenMinimalApiEndpointResponds_ThenHttpJsonOptionsStillApply() + { + using var factory = CreateFactory(useNewtonsoftJson: true); + using var client = factory.CreateClient(); + + var payload = await PostAsync(client, "/api/Product/create"); + + // MVC options and Http JSON options do not cascade: the minimal API + // endpoint keeps serializing with System.Text.Json. + CollectionAssert.AreEquivalent( + new[] { "id", "price", "quantity", "releaseDate", "manufacturer" }, + payload.Select(property => property.Key).ToArray()); + Assert.AreNotEqual("14-04-2024", (string?)payload["releaseDate"]); + } +} diff --git a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/ProductControllerUnitTests.cs b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/ProductControllerUnitTests.cs index d49a23d6e6..f803382284 100644 --- a/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/ProductControllerUnitTests.cs +++ b/json-csharp/GlobalDefaultJsonSerializationoptions/GlobalDefaultJsonSerializationoptionsUnitTests/ProductControllerUnitTests.cs @@ -2,7 +2,6 @@ using GlobalDefaultJsonSerializationoptions.Controllers; using Microsoft.AspNetCore.Mvc; using Microsoft.VisualStudio.TestTools.UnitTesting; -using Newtonsoft.Json; namespace GlobalDefaultJsonSerializationoptionsUnitTests; @@ -42,8 +41,6 @@ public void GivenValidProduct_WhenSaveProductIsCalled_ThenReturnsProductWithSame // Assert Assert.IsNotNull(result); Assert.AreEqual(200, result.StatusCode); - - var expectedJson = JsonConvert.SerializeObject(product); - Assert.AreEqual(expectedJson, result?.Value?.ToString()); + Assert.AreEqual(product, result.Value); } }