From 87d22f3109b86d3493f2af136f22199b253a1107 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 17 Feb 2026 05:21:00 +0100 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=85=20add=20tests=20for=20json=20and?= =?UTF-8?q?=20xml=20formatter=20options=20registration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ServiceCollectionExtensionsTest.cs | 33 ++++++++++++ .../ServiceCollectionExtensionsTest.cs | 33 ++++++++++++ .../ServiceCollectionExtensionsTest.cs | 51 +++++++++++++++++++ 3 files changed, 117 insertions(+) create mode 100644 test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/Formatters/ServiceCollectionExtensionsTest.cs create mode 100644 test/Cuemon.Extensions.AspNetCore.Tests/Xml/Formatters/ServiceCollectionExtensionsTest.cs diff --git a/test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/Formatters/ServiceCollectionExtensionsTest.cs b/test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/Formatters/ServiceCollectionExtensionsTest.cs new file mode 100644 index 00000000..0c8c0c56 --- /dev/null +++ b/test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/Formatters/ServiceCollectionExtensionsTest.cs @@ -0,0 +1,33 @@ +using System.Linq; +using Cuemon.Extensions.Text.Json.Formatters; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Cuemon.Extensions.AspNetCore.Text.Json.Formatters +{ + public class ServiceCollectionExtensionsTest : Test + { + public ServiceCollectionExtensionsTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void AddJsonFormatterOptions_ShouldOnlyRegisterOnce_WhenCalledMultipleTimes() + { + var sut = new ServiceCollection(); + + sut.AddJsonFormatterOptions(); + sut.AddJsonFormatterOptions(); + sut.AddJsonFormatterOptions(); + + var configureOptionsCount = sut.Count(sd => + sd.ServiceType == typeof(IConfigureOptions)); + + TestOutput.WriteLine($"IConfigureOptions registrations: {configureOptionsCount}"); + + Assert.Equal(1, configureOptionsCount); + } + } +} diff --git a/test/Cuemon.Extensions.AspNetCore.Tests/Xml/Formatters/ServiceCollectionExtensionsTest.cs b/test/Cuemon.Extensions.AspNetCore.Tests/Xml/Formatters/ServiceCollectionExtensionsTest.cs new file mode 100644 index 00000000..3f9846f2 --- /dev/null +++ b/test/Cuemon.Extensions.AspNetCore.Tests/Xml/Formatters/ServiceCollectionExtensionsTest.cs @@ -0,0 +1,33 @@ +using System.Linq; +using Cuemon.Xml.Serialization.Formatters; +using Codebelt.Extensions.Xunit; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Cuemon.Extensions.AspNetCore.Xml.Formatters +{ + public class ServiceCollectionExtensionsTest : Test + { + public ServiceCollectionExtensionsTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void AddXmlFormatterOptions_ShouldOnlyRegisterOnce_WhenCalledMultipleTimes() + { + var sut = new ServiceCollection(); + + sut.AddXmlFormatterOptions(); + sut.AddXmlFormatterOptions(); + sut.AddXmlFormatterOptions(); + + var configureOptionsCount = sut.Count(sd => + sd.ServiceType == typeof(IConfigureOptions)); + + TestOutput.WriteLine($"IConfigureOptions registrations: {configureOptionsCount}"); + + Assert.Equal(1, configureOptionsCount); + } + } +} diff --git a/test/Cuemon.Extensions.DependencyInjection.Tests/ServiceCollectionExtensionsTest.cs b/test/Cuemon.Extensions.DependencyInjection.Tests/ServiceCollectionExtensionsTest.cs index 632f9759..b6fe1040 100644 --- a/test/Cuemon.Extensions.DependencyInjection.Tests/ServiceCollectionExtensionsTest.cs +++ b/test/Cuemon.Extensions.DependencyInjection.Tests/ServiceCollectionExtensionsTest.cs @@ -624,5 +624,56 @@ public void SynchronizeOptions_ShouldChangeAllWithAServiceTypeHavingFaultDescrip #endif + [Fact] + public void Add_ShouldOnlyRegisterOptionsOnce_WhenCalledMultipleTimesWithSameOptionsType() + { + var sut = new ServiceCollection(); + + sut.Add(typeof(FakeService), typeof(FakeServiceScoped), ServiceLifetime.Scoped, (Action)(o => o.Greeting = "First")); + sut.Add(typeof(FakeService), typeof(FakeServiceSingleton), ServiceLifetime.Singleton, (Action)(o => o.Greeting = "Second")); + sut.Add(typeof(FakeService), typeof(FakeServiceTransient), ServiceLifetime.Transient, (Action)(o => o.Greeting = "Third")); + + var configureOptionsCount = sut.Count(sd => + sd.ServiceType == typeof(IConfigureOptions)); + + TestOutput.WriteLine($"IConfigureOptions registrations: {configureOptionsCount}"); + + Assert.Equal(1, configureOptionsCount); + } + + [Fact] + public void TryAdd_ShouldOnlyRegisterOptionsOnce_WhenCalledMultipleTimesWithSameOptionsType() + { + var sut = new ServiceCollection(); + + sut.TryAdd(typeof(FakeService), typeof(FakeServiceScoped), ServiceLifetime.Scoped, (Action)(o => o.Greeting = "First")); + sut.TryAdd(typeof(FakeService), typeof(FakeServiceSingleton), ServiceLifetime.Singleton, (Action)(o => o.Greeting = "Second")); + sut.TryAdd(typeof(FakeService), typeof(FakeServiceTransient), ServiceLifetime.Transient, (Action)(o => o.Greeting = "Third")); + + var configureOptionsCount = sut.Count(sd => + sd.ServiceType == typeof(IConfigureOptions)); + + TestOutput.WriteLine($"IConfigureOptions registrations: {configureOptionsCount}"); + + Assert.Equal(1, configureOptionsCount); + } + + [Fact] + public void TryAdd_WithFactory_ShouldOnlyRegisterOptionsOnce_WhenCalledMultipleTimesWithSameOptionsType() + { + var sut = new ServiceCollection(); + + sut.TryAdd(typeof(FakeService), _ => new FakeServiceScoped(default), ServiceLifetime.Scoped, (Action)(o => o.Greeting = "First")); + sut.TryAdd(typeof(FakeService), _ => new FakeServiceSingleton(default), ServiceLifetime.Singleton, (Action)(o => o.Greeting = "Second")); + sut.TryAdd(typeof(FakeService), _ => new FakeServiceTransient(default), ServiceLifetime.Transient, (Action)(o => o.Greeting = "Third")); + + var configureOptionsCount = sut.Count(sd => + sd.ServiceType == typeof(IConfigureOptions)); + + TestOutput.WriteLine($"IConfigureOptions registrations: {configureOptionsCount}"); + + Assert.Equal(1, configureOptionsCount); + } + } } From 0764213ad5fdd48767140393d8d46069860a08e0 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 17 Feb 2026 05:22:17 +0100 Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=90=9B=20use=20TryConfigure=20instead?= =?UTF-8?q?=20of=20Configure=20in=20service=20collection=20extensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Formatters/ServiceCollectionExtensions.cs | 3 ++- .../Formatters/ServiceCollectionExtensions.cs | 3 ++- .../ServiceCollectionExtensions.cs | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Cuemon.Extensions.AspNetCore.Text.Json/Formatters/ServiceCollectionExtensions.cs b/src/Cuemon.Extensions.AspNetCore.Text.Json/Formatters/ServiceCollectionExtensions.cs index dd4e295c..ab2014fd 100644 --- a/src/Cuemon.Extensions.AspNetCore.Text.Json/Formatters/ServiceCollectionExtensions.cs +++ b/src/Cuemon.Extensions.AspNetCore.Text.Json/Formatters/ServiceCollectionExtensions.cs @@ -3,6 +3,7 @@ using System.Text.Json; using Cuemon.AspNetCore.Diagnostics; using Cuemon.Extensions.AspNetCore.Text.Json.Converters; +using Cuemon.Extensions.DependencyInjection; using Cuemon.Extensions.Text.Json.Formatters; using Cuemon.Net.Http; using Microsoft.Extensions.DependencyInjection; @@ -37,7 +38,7 @@ public static IServiceCollection AddJsonFormatterOptions(this IServiceCollection { Validator.ThrowIfNull(services); Validator.ThrowIfInvalidConfigurator(setup, out var options); - services.Configure(setup ?? (o => + services.TryConfigure(setup ?? (o => { o.Settings = options.Settings; o.SensitivityDetails = options.SensitivityDetails; diff --git a/src/Cuemon.Extensions.AspNetCore.Xml/Formatters/ServiceCollectionExtensions.cs b/src/Cuemon.Extensions.AspNetCore.Xml/Formatters/ServiceCollectionExtensions.cs index d11c9d2c..be5a220d 100644 --- a/src/Cuemon.Extensions.AspNetCore.Xml/Formatters/ServiceCollectionExtensions.cs +++ b/src/Cuemon.Extensions.AspNetCore.Xml/Formatters/ServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using System.Net.Http; using Cuemon.AspNetCore.Diagnostics; using Cuemon.Extensions.AspNetCore.Xml.Converters; +using Cuemon.Extensions.DependencyInjection; using Cuemon.Net.Http; using Cuemon.Xml.Serialization.Formatters; using Microsoft.Extensions.DependencyInjection; @@ -36,7 +37,7 @@ public static IServiceCollection AddXmlFormatterOptions(this IServiceCollection { Validator.ThrowIfNull(services); Validator.ThrowIfInvalidConfigurator(setup, out var options); - services.Configure(setup ?? (o => + services.TryConfigure(setup ?? (o => { o.Settings = options.Settings; o.SensitivityDetails = options.SensitivityDetails; diff --git a/src/Cuemon.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Cuemon.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 6912e2fb..7f20e78a 100644 --- a/src/Cuemon.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Cuemon.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -398,7 +398,7 @@ public static IServiceCollection TryAdd(this IServiceCollection servic Validator.ThrowIfNull(services); Validator.ThrowIfNull(setup); services.AddServices(service, implementation, lifetime, true); - services.Configure(setup); + services.TryConfigure(setup); return services; } @@ -475,7 +475,7 @@ public static IServiceCollection TryAdd(this IServiceCollection servic Validator.ThrowIfNull(implementationFactory); Validator.ThrowIfNull(setup); services.AddServices(service, implementationFactory, lifetime, true); - services.Configure(setup); + services.TryConfigure(setup); return services; } From 801a1bb41ab54fe98ec13f449a669bffcfad6a84 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 17 Feb 2026 18:31:28 +0100 Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=90=9B=20use=20tryconfigure=20instead?= =?UTF-8?q?=20of=20configure=20in=20service=20collection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ServiceCollectionExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cuemon.Extensions.DependencyInjection/ServiceCollectionExtensions.cs b/src/Cuemon.Extensions.DependencyInjection/ServiceCollectionExtensions.cs index 7f20e78a..60d95362 100644 --- a/src/Cuemon.Extensions.DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Cuemon.Extensions.DependencyInjection/ServiceCollectionExtensions.cs @@ -76,7 +76,7 @@ public static IServiceCollection Add(this IServiceCollection services, Validator.ThrowIfNull(services); Validator.ThrowIfNull(setup); services.AddServices(service, implementation, lifetime, false); - services.Configure(setup); + services.TryConfigure(setup); return services; } From 8c00056cbf4aa1f8a874d7bec8c643e849ebeb64 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 17 Feb 2026 21:13:08 +0100 Subject: [PATCH 4/8] =?UTF-8?q?=E2=9C=A8=20add=20minimaljsonoptions=20and?= =?UTF-8?q?=20service=20collection=20extensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MinimalJsonOptions.cs | 59 +++++++++++++++++++ .../ServiceCollectionExtensions.cs | 36 +++++++++++ 2 files changed, 95 insertions(+) create mode 100644 src/Cuemon.Extensions.AspNetCore.Text.Json/MinimalJsonOptions.cs create mode 100644 src/Cuemon.Extensions.AspNetCore.Text.Json/ServiceCollectionExtensions.cs diff --git a/src/Cuemon.Extensions.AspNetCore.Text.Json/MinimalJsonOptions.cs b/src/Cuemon.Extensions.AspNetCore.Text.Json/MinimalJsonOptions.cs new file mode 100644 index 00000000..fffff6d2 --- /dev/null +++ b/src/Cuemon.Extensions.AspNetCore.Text.Json/MinimalJsonOptions.cs @@ -0,0 +1,59 @@ +using Cuemon.Collections.Generic; +using Cuemon.Extensions.AspNetCore.Text.Json.Converters; +using Cuemon.Extensions.Text.Json.Formatters; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.Options; + +namespace Cuemon.Extensions.AspNetCore.Text.Json +{ + /// + /// A implementation which will pass to . + /// + public class MinimalJsonOptions : ConfigureOptions + { + /// + /// Creates a new . + /// + public MinimalJsonOptions(IOptions formatterOptions) : base(mo => + { + var options = formatterOptions.Value; + + options.Settings.Converters.AddHttpExceptionDescriptorConverter(o => o.SensitivityDetails = options.SensitivityDetails); + + Decorator.Enclose(mo.SerializerOptions.Converters).AddRange(options.Settings.Converters); + mo.SerializerOptions.AllowOutOfOrderMetadataProperties = options.Settings.AllowOutOfOrderMetadataProperties; + mo.SerializerOptions.AllowTrailingCommas = options.Settings.AllowTrailingCommas; + mo.SerializerOptions.DefaultBufferSize = options.Settings.DefaultBufferSize; + mo.SerializerOptions.Encoder = options.Settings.Encoder; + mo.SerializerOptions.DictionaryKeyPolicy = options.Settings.DictionaryKeyPolicy; + mo.SerializerOptions.DefaultIgnoreCondition = options.Settings.DefaultIgnoreCondition; + mo.SerializerOptions.NumberHandling = options.Settings.NumberHandling; + mo.SerializerOptions.PreferredObjectCreationHandling = options.Settings.PreferredObjectCreationHandling; + mo.SerializerOptions.UnknownTypeHandling = options.Settings.UnknownTypeHandling; + mo.SerializerOptions.UnmappedMemberHandling = options.Settings.UnmappedMemberHandling; + mo.SerializerOptions.IgnoreReadOnlyProperties = options.Settings.IgnoreReadOnlyProperties; + mo.SerializerOptions.IgnoreReadOnlyFields = options.Settings.IgnoreReadOnlyFields; + mo.SerializerOptions.IncludeFields = options.Settings.IncludeFields; + mo.SerializerOptions.MaxDepth = options.Settings.MaxDepth; + mo.SerializerOptions.PropertyNamingPolicy = options.Settings.PropertyNamingPolicy; + mo.SerializerOptions.PropertyNameCaseInsensitive = options.Settings.PropertyNameCaseInsensitive; + mo.SerializerOptions.ReadCommentHandling = options.Settings.ReadCommentHandling; + mo.SerializerOptions.WriteIndented = options.Settings.WriteIndented; + mo.SerializerOptions.IndentCharacter = options.Settings.IndentCharacter; + mo.SerializerOptions.IndentSize = options.Settings.IndentSize; + mo.SerializerOptions.ReferenceHandler = options.Settings.ReferenceHandler; + mo.SerializerOptions.NewLine = options.Settings.NewLine; + mo.SerializerOptions.RespectNullableAnnotations = options.Settings.RespectNullableAnnotations; + mo.SerializerOptions.RespectRequiredConstructorParameters = options.Settings.RespectRequiredConstructorParameters; +#if NET10_0_OR_GREATER + mo.SerializerOptions.AllowDuplicateProperties = options.Settings.AllowDuplicateProperties; +#endif + if (options.Settings.TypeInfoResolver is not null) + { + mo.SerializerOptions.TypeInfoResolver = options.Settings.TypeInfoResolver; + } + }) + { + } + } +} diff --git a/src/Cuemon.Extensions.AspNetCore.Text.Json/ServiceCollectionExtensions.cs b/src/Cuemon.Extensions.AspNetCore.Text.Json/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..b7c0eae1 --- /dev/null +++ b/src/Cuemon.Extensions.AspNetCore.Text.Json/ServiceCollectionExtensions.cs @@ -0,0 +1,36 @@ +using System; +using Cuemon.Extensions.AspNetCore.Text.Json.Formatters; +using Cuemon.Extensions.Text.Json.Formatters; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; + +namespace Cuemon.Extensions.AspNetCore.Text.Json +{ + /// + /// Extension methods for the interface. + /// + public static class ServiceCollectionExtensions + { + /// + /// Adds a service to the specified . + /// + /// The to add services to. + /// The which may be configured. + /// An that can be used to further configure other services. + /// + /// This method registers a configuration as a singleton for + /// and delegates to to configure the JSON exception response formatter. + /// + /// + /// cannot be null. + /// + public static IServiceCollection AddMinimalJsonOptions(this IServiceCollection services, Action setup = null) + { + Validator.ThrowIfNull(services); + services.TryAddEnumerable(ServiceDescriptor.Singleton, MinimalJsonOptions>()); + return services.AddJsonExceptionResponseFormatter(setup); + } + } +} From 8044a450f0724b46d4b58049b5f3867a97bf8c68 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 17 Feb 2026 21:13:16 +0100 Subject: [PATCH 5/8] =?UTF-8?q?=E2=9C=85=20add=20tests=20for=20minimaljson?= =?UTF-8?q?options=20and=20service=20collection=20extensions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Text.Json/MinimalJsonOptionsTest.cs | 291 ++++++++++++++++++ .../ServiceCollectionExtensionsTest.cs | 82 +++++ 2 files changed, 373 insertions(+) create mode 100644 test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/MinimalJsonOptionsTest.cs create mode 100644 test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/ServiceCollectionExtensionsTest.cs diff --git a/test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/MinimalJsonOptionsTest.cs b/test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/MinimalJsonOptionsTest.cs new file mode 100644 index 00000000..80c90aa3 --- /dev/null +++ b/test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/MinimalJsonOptionsTest.cs @@ -0,0 +1,291 @@ +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using Codebelt.Extensions.Xunit; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Cuemon.Extensions.AspNetCore.Text.Json +{ + public class MinimalJsonOptionsTest : Test + { + public MinimalJsonOptionsTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagatePropertyNamingPolicy_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.Equal(JsonNamingPolicy.SnakeCaseLower, jsonOptions.SerializerOptions.PropertyNamingPolicy); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagateWriteIndented_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.WriteIndented = false; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.False(jsonOptions.SerializerOptions.WriteIndented); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagateDictionaryKeyPolicy_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.DictionaryKeyPolicy = JsonNamingPolicy.KebabCaseLower; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.Equal(JsonNamingPolicy.KebabCaseLower, jsonOptions.SerializerOptions.DictionaryKeyPolicy); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagateDefaultIgnoreCondition_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.DefaultIgnoreCondition = JsonIgnoreCondition.Never; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.Equal(JsonIgnoreCondition.Never, jsonOptions.SerializerOptions.DefaultIgnoreCondition); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagateMaxDepth_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.MaxDepth = 128; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.Equal(128, jsonOptions.SerializerOptions.MaxDepth); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagateDefaultBufferSize_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.DefaultBufferSize = 8192; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.Equal(8192, jsonOptions.SerializerOptions.DefaultBufferSize); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagateReadCommentHandling_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.ReadCommentHandling = JsonCommentHandling.Disallow; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.Equal(JsonCommentHandling.Disallow, jsonOptions.SerializerOptions.ReadCommentHandling); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagateAllowTrailingCommas_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.AllowTrailingCommas = true; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.True(jsonOptions.SerializerOptions.AllowTrailingCommas); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagateNumberHandling_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.NumberHandling = JsonNumberHandling.AllowReadingFromString; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.Equal(JsonNumberHandling.AllowReadingFromString, jsonOptions.SerializerOptions.NumberHandling); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagatePropertyNameCaseInsensitive_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.PropertyNameCaseInsensitive = true; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.True(jsonOptions.SerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagateIncludeFields_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.IncludeFields = true; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.True(jsonOptions.SerializerOptions.IncludeFields); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagateIgnoreReadOnlyProperties_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.IgnoreReadOnlyProperties = true; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.True(jsonOptions.SerializerOptions.IgnoreReadOnlyProperties); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagateEncoder_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.Encoder = JavaScriptEncoder.Default; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.Equal(JavaScriptEncoder.Default, jsonOptions.SerializerOptions.Encoder); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagateReferenceHandler_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.ReferenceHandler = ReferenceHandler.Preserve; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.Equal(ReferenceHandler.Preserve, jsonOptions.SerializerOptions.ReferenceHandler); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagateConverters_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + TestOutput.WriteLine($"Converter count: {jsonOptions.SerializerOptions.Converters.Count}"); + + Assert.NotEmpty(jsonOptions.SerializerOptions.Converters); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagateTypeInfoResolver_WhenNotNull() + { + var resolver = new DefaultJsonTypeInfoResolver(); + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.TypeInfoResolver = resolver; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.Same(resolver, jsonOptions.SerializerOptions.TypeInfoResolver); + } + + [Fact] + public void MinimalJsonOptions_ShouldNotOverrideTypeInfoResolver_WhenNull() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.TypeInfoResolver = null; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + // When source TypeInfoResolver is null, the target's existing resolver should not be overridden + // (the target may or may not have its own default resolver, so we just verify no exception) + TestOutput.WriteLine($"TypeInfoResolver is null: {jsonOptions.SerializerOptions.TypeInfoResolver is null}"); + } + + [Fact] + public void MinimalJsonOptions_ShouldPropagateIndentSize_FromJsonFormatterOptions() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(o => + { + o.Settings.IndentSize = 4; + }); + + var provider = services.BuildServiceProvider(); + var jsonOptions = provider.GetRequiredService>().Value; + + Assert.Equal(4, jsonOptions.SerializerOptions.IndentSize); + } + } +} diff --git a/test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/ServiceCollectionExtensionsTest.cs b/test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/ServiceCollectionExtensionsTest.cs new file mode 100644 index 00000000..8dda2f5b --- /dev/null +++ b/test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/ServiceCollectionExtensionsTest.cs @@ -0,0 +1,82 @@ +using System.Linq; +using Cuemon.AspNetCore.Diagnostics; +using Cuemon.Extensions.AspNetCore.Diagnostics; +using Cuemon.Extensions.Text.Json.Formatters; +using Codebelt.Extensions.Xunit; +using Microsoft.AspNetCore.Http.Json; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Cuemon.Extensions.AspNetCore.Text.Json +{ + public class ServiceCollectionExtensionsTest : Test + { + public ServiceCollectionExtensionsTest(ITestOutputHelper output) : base(output) + { + } + + [Fact] + public void AddMinimalJsonOptions_ShouldRegisterMinimalJsonOptionsAsSingleton() + { + var sut = new ServiceCollection(); + + sut.AddMinimalJsonOptions(); + + var descriptor = sut.Single(sd => + sd.ServiceType == typeof(IConfigureOptions) && + sd.ImplementationType == typeof(MinimalJsonOptions)); + + TestOutput.WriteLine($"Lifetime: {descriptor.Lifetime}"); + + Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime); + } + + [Fact] + public void AddMinimalJsonOptions_ShouldRegisterMinimalJsonOptionsOnlyOnce_WhenCalledMultipleTimes() + { + var sut = new ServiceCollection(); + + sut.AddMinimalJsonOptions(); + sut.AddMinimalJsonOptions(); + sut.AddMinimalJsonOptions(); + + var count = sut.Count(sd => + sd.ServiceType == typeof(IConfigureOptions) && + sd.ImplementationType == typeof(MinimalJsonOptions)); + + TestOutput.WriteLine($"MinimalJsonOptions registrations: {count}"); + + Assert.Equal(1, count); + } + + [Fact] + public void AddMinimalJsonOptions_ShouldAlsoRegisterJsonExceptionResponseFormatter() + { + var sut = new ServiceCollection(); + sut.AddFaultDescriptorOptions(); + + sut.AddMinimalJsonOptions(); + + var hasFormatter = sut.Any(sd => + sd.ServiceType == typeof(HttpExceptionDescriptorResponseFormatter)); + + Assert.True(hasFormatter); + } + + [Fact] + public void AddMinimalJsonOptions_ShouldRegisterJsonFormatterOptions() + { + var sut = new ServiceCollection(); + + sut.AddMinimalJsonOptions(); + + var count = sut.Count(sd => + sd.ServiceType == typeof(IConfigureOptions)); + + TestOutput.WriteLine($"IConfigureOptions registrations: {count}"); + + Assert.True(count >= 1); + } + } +} From 426be6a10443148b5e98710ea7f78287df8a09d1 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 17 Feb 2026 22:13:53 +0100 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=90=9B=20refactor=20minimaljsonoption?= =?UTF-8?q?s=20to=20use=20jsonserializeroptions=20directly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MinimalJsonOptions.cs | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/src/Cuemon.Extensions.AspNetCore.Text.Json/MinimalJsonOptions.cs b/src/Cuemon.Extensions.AspNetCore.Text.Json/MinimalJsonOptions.cs index fffff6d2..685673f1 100644 --- a/src/Cuemon.Extensions.AspNetCore.Text.Json/MinimalJsonOptions.cs +++ b/src/Cuemon.Extensions.AspNetCore.Text.Json/MinimalJsonOptions.cs @@ -1,4 +1,5 @@ -using Cuemon.Collections.Generic; +using System.Text.Json; +using Cuemon.Collections.Generic; using Cuemon.Extensions.AspNetCore.Text.Json.Converters; using Cuemon.Extensions.Text.Json.Formatters; using Microsoft.AspNetCore.Http.Json; @@ -18,39 +19,40 @@ public MinimalJsonOptions(IOptions formatterOptions) : bas { var options = formatterOptions.Value; - options.Settings.Converters.AddHttpExceptionDescriptorConverter(o => o.SensitivityDetails = options.SensitivityDetails); + var settings = new JsonSerializerOptions(options.Settings); + settings.Converters.AddHttpExceptionDescriptorConverter(o => o.SensitivityDetails = options.SensitivityDetails); - Decorator.Enclose(mo.SerializerOptions.Converters).AddRange(options.Settings.Converters); - mo.SerializerOptions.AllowOutOfOrderMetadataProperties = options.Settings.AllowOutOfOrderMetadataProperties; - mo.SerializerOptions.AllowTrailingCommas = options.Settings.AllowTrailingCommas; - mo.SerializerOptions.DefaultBufferSize = options.Settings.DefaultBufferSize; - mo.SerializerOptions.Encoder = options.Settings.Encoder; - mo.SerializerOptions.DictionaryKeyPolicy = options.Settings.DictionaryKeyPolicy; - mo.SerializerOptions.DefaultIgnoreCondition = options.Settings.DefaultIgnoreCondition; - mo.SerializerOptions.NumberHandling = options.Settings.NumberHandling; - mo.SerializerOptions.PreferredObjectCreationHandling = options.Settings.PreferredObjectCreationHandling; - mo.SerializerOptions.UnknownTypeHandling = options.Settings.UnknownTypeHandling; - mo.SerializerOptions.UnmappedMemberHandling = options.Settings.UnmappedMemberHandling; - mo.SerializerOptions.IgnoreReadOnlyProperties = options.Settings.IgnoreReadOnlyProperties; - mo.SerializerOptions.IgnoreReadOnlyFields = options.Settings.IgnoreReadOnlyFields; - mo.SerializerOptions.IncludeFields = options.Settings.IncludeFields; - mo.SerializerOptions.MaxDepth = options.Settings.MaxDepth; - mo.SerializerOptions.PropertyNamingPolicy = options.Settings.PropertyNamingPolicy; - mo.SerializerOptions.PropertyNameCaseInsensitive = options.Settings.PropertyNameCaseInsensitive; - mo.SerializerOptions.ReadCommentHandling = options.Settings.ReadCommentHandling; - mo.SerializerOptions.WriteIndented = options.Settings.WriteIndented; - mo.SerializerOptions.IndentCharacter = options.Settings.IndentCharacter; - mo.SerializerOptions.IndentSize = options.Settings.IndentSize; - mo.SerializerOptions.ReferenceHandler = options.Settings.ReferenceHandler; - mo.SerializerOptions.NewLine = options.Settings.NewLine; - mo.SerializerOptions.RespectNullableAnnotations = options.Settings.RespectNullableAnnotations; - mo.SerializerOptions.RespectRequiredConstructorParameters = options.Settings.RespectRequiredConstructorParameters; + Decorator.Enclose(mo.SerializerOptions.Converters).AddRange(settings.Converters); + mo.SerializerOptions.AllowOutOfOrderMetadataProperties = settings.AllowOutOfOrderMetadataProperties; + mo.SerializerOptions.AllowTrailingCommas = settings.AllowTrailingCommas; + mo.SerializerOptions.DefaultBufferSize = settings.DefaultBufferSize; + mo.SerializerOptions.Encoder = settings.Encoder; + mo.SerializerOptions.DictionaryKeyPolicy = settings.DictionaryKeyPolicy; + mo.SerializerOptions.DefaultIgnoreCondition = settings.DefaultIgnoreCondition; + mo.SerializerOptions.NumberHandling = settings.NumberHandling; + mo.SerializerOptions.PreferredObjectCreationHandling = settings.PreferredObjectCreationHandling; + mo.SerializerOptions.UnknownTypeHandling = settings.UnknownTypeHandling; + mo.SerializerOptions.UnmappedMemberHandling = settings.UnmappedMemberHandling; + mo.SerializerOptions.IgnoreReadOnlyProperties = settings.IgnoreReadOnlyProperties; + mo.SerializerOptions.IgnoreReadOnlyFields = settings.IgnoreReadOnlyFields; + mo.SerializerOptions.IncludeFields = settings.IncludeFields; + mo.SerializerOptions.MaxDepth = settings.MaxDepth; + mo.SerializerOptions.PropertyNamingPolicy = settings.PropertyNamingPolicy; + mo.SerializerOptions.PropertyNameCaseInsensitive = settings.PropertyNameCaseInsensitive; + mo.SerializerOptions.ReadCommentHandling = settings.ReadCommentHandling; + mo.SerializerOptions.WriteIndented = settings.WriteIndented; + mo.SerializerOptions.IndentCharacter = settings.IndentCharacter; + mo.SerializerOptions.IndentSize = settings.IndentSize; + mo.SerializerOptions.ReferenceHandler = settings.ReferenceHandler; + mo.SerializerOptions.NewLine = settings.NewLine; + mo.SerializerOptions.RespectNullableAnnotations = settings.RespectNullableAnnotations; + mo.SerializerOptions.RespectRequiredConstructorParameters = settings.RespectRequiredConstructorParameters; #if NET10_0_OR_GREATER - mo.SerializerOptions.AllowDuplicateProperties = options.Settings.AllowDuplicateProperties; + mo.SerializerOptions.AllowDuplicateProperties = settings.AllowDuplicateProperties; #endif - if (options.Settings.TypeInfoResolver is not null) + if (settings.TypeInfoResolver is not null) { - mo.SerializerOptions.TypeInfoResolver = options.Settings.TypeInfoResolver; + mo.SerializerOptions.TypeInfoResolver = settings.TypeInfoResolver; } }) { From 8b580dc5f6826c7ce2ab3b13ba1c78429ba01681 Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 17 Feb 2026 22:14:03 +0100 Subject: [PATCH 7/8] =?UTF-8?q?=E2=9C=85=20add=20test=20to=20ensure=20mini?= =?UTF-8?q?maljsonoptions=20does=20not=20duplicate=20converters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Text.Json/MinimalJsonOptionsTest.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/MinimalJsonOptionsTest.cs b/test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/MinimalJsonOptionsTest.cs index 80c90aa3..baad06a6 100644 --- a/test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/MinimalJsonOptionsTest.cs +++ b/test/Cuemon.Extensions.AspNetCore.Tests/Text.Json/MinimalJsonOptionsTest.cs @@ -1,6 +1,7 @@ using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; +using Cuemon.Extensions.Text.Json.Formatters; using System.Text.Json.Serialization.Metadata; using Codebelt.Extensions.Xunit; using Microsoft.AspNetCore.Http.Json; @@ -240,6 +241,26 @@ public void MinimalJsonOptions_ShouldPropagateConverters_FromJsonFormatterOption Assert.NotEmpty(jsonOptions.SerializerOptions.Converters); } + [Fact] + public void MinimalJsonOptions_ShouldNotDuplicateConverters_WhenOptionsCreatedMultipleTimes() + { + var services = new ServiceCollection(); + services.AddMinimalJsonOptions(); + + var provider = services.BuildServiceProvider(); + var formatterOptions = provider.GetRequiredService>().Value; + var optionsFactory = provider.GetRequiredService>(); + + var before = formatterOptions.Settings.Converters.Count; + + optionsFactory.Create(Options.DefaultName); + optionsFactory.Create(Options.DefaultName); + + var after = formatterOptions.Settings.Converters.Count; + + Assert.Equal(before, after); + } + [Fact] public void MinimalJsonOptions_ShouldPropagateTypeInfoResolver_WhenNotNull() { From 813574452da32f69b274b3d53c54d400266b6cee Mon Sep 17 00:00:00 2001 From: Michael Mortensen Date: Tue, 17 Feb 2026 22:53:20 +0100 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=93=9D=20update=20documentation=20for?= =?UTF-8?q?=20MinimalJsonOptions=20constructor=20parameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../MinimalJsonOptions.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Cuemon.Extensions.AspNetCore.Text.Json/MinimalJsonOptions.cs b/src/Cuemon.Extensions.AspNetCore.Text.Json/MinimalJsonOptions.cs index 685673f1..ddf2cfc7 100644 --- a/src/Cuemon.Extensions.AspNetCore.Text.Json/MinimalJsonOptions.cs +++ b/src/Cuemon.Extensions.AspNetCore.Text.Json/MinimalJsonOptions.cs @@ -13,8 +13,11 @@ namespace Cuemon.Extensions.AspNetCore.Text.Json public class MinimalJsonOptions : ConfigureOptions { /// - /// Creates a new . + /// Initializes a new instance of the class. /// + /// + /// The formatter options. + /// public MinimalJsonOptions(IOptions formatterOptions) : base(mo => { var options = formatterOptions.Value;