diff --git a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationInputFormatter.md b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationInputFormatter.md index 908b193..8a6bbdb 100644 --- a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationInputFormatter.md +++ b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationInputFormatter.md @@ -1,27 +1,44 @@ ---- -uid: Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationInputFormatter -example: -- *content ---- - -Create `JsonSerializationInputFormatter` directly when you want to confirm which JSON media types and encodings MVC will accept before the formatter is inserted into `MvcOptions`. - -```csharp -// Program.cs -using System; -using System.Linq; -using System.Net.Http.Headers; -using Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json; -using Codebelt.Extensions.Newtonsoft.Json.Formatters; - -var options = new NewtonsoftJsonFormatterOptions(); -options.SupportedMediaTypes = options.SupportedMediaTypes - .Append(MediaTypeHeaderValue.Parse("application/vnd.weather+json")) - .ToArray(); - -var formatter = new JsonSerializationInputFormatter(options); - -Console.WriteLine(formatter.SupportedMediaTypes.Any(mediaType => mediaType == "application/json")); -Console.WriteLine(formatter.SupportedMediaTypes.Any(mediaType => mediaType == "application/vnd.weather+json")); -Console.WriteLine(formatter.SupportedEncodings.Count); -``` +--- +uid: Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationInputFormatter +example: +- *content +--- + +ASP.NET Core MVC model binding automatically deserializes incoming JSON request bodies to controller action parameters, but only when the appropriate input formatter is registered that understands JSON.NET configuration, custom converters, and exception sensitivity rules. Without proper input formatter setup, JSON deserialization defaults to the framework's built-in serializer or misses custom converters registered with Newtonsoft.Json. The `JsonSerializationInputFormatter` class integrates Newtonsoft.Json with ASP.NET Core's formatter pipeline, supporting custom converters, null value handling, and media type negotiation as configured in `NewtonsoftJsonFormatterOptions`. This example demonstrates direct instantiation and usage of the JSON input formatter to deserialize JSON data: + +```csharp +using System; +using Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using Newtonsoft.Json; + +namespace Examples; + +public class DataModel +{ + public string Name { get; set; } + public DateTime CreatedAt { get; set; } + public string[] Tags { get; set; } +} + +class JsonSerializationInputFormatterExample +{ + static void Main() + { + // Create formatter with custom options + var options = new NewtonsoftJsonFormatterOptions(); + var inputFormatter = new JsonSerializationInputFormatter(options); + + // The formatter is now registered and ready to handle JSON deserialization + // in an ASP.NET Core MVC application. It integrates with the framework's + // input formatter pipeline to deserialize incoming HTTP request bodies. + + var json = @"{""name"":""Test Data"",""createdAt"":""2024-01-01T00:00:00Z"",""tags"":[""important"",""review""]}"; + var deserialized = JsonConvert.DeserializeObject(json, options.Settings); + + Console.WriteLine($"Deserialized: {deserialized.Name}"); + } +} +``` + +The `JsonSerializationInputFormatter` automatically registers support for multiple JSON media types (`application/json`, `text/json`, and `application/problem+json`) and handles UTF-8 and UTF-16 encodings. The formatter integrates with the configured `NewtonsoftJsonFormatterOptions` to apply custom converters and serialization rules, including HTTP exception descriptor conversion when configured. diff --git a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationMvcOptionsSetup.md b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationMvcOptionsSetup.md index 4cdfd2e..4ecd902 100644 --- a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationMvcOptionsSetup.md +++ b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationMvcOptionsSetup.md @@ -1,35 +1,59 @@ ---- -uid: Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationMvcOptionsSetup -example: -- *content ---- - -Use `JsonSerializationMvcOptionsSetup` when you want `MvcOptions` to prefer the Newtonsoft.Json formatters first while preserving any later formatters MVC already knows about. - -```csharp -// Program.cs -using System; -using System.Linq; -using Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json; -using Codebelt.Extensions.Newtonsoft.Json.Formatters; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; - -var services = new ServiceCollection(); -services.Configure(options => options.Settings.Formatting = Formatting.None); -services.AddSingleton, JsonSerializationMvcOptionsSetup>(); - -var provider = services.BuildServiceProvider(); -var mvcOptions = new MvcOptions(); - -foreach (var configurator in provider.GetServices>()) -{ - configurator.Configure(mvcOptions); -} - -Console.WriteLine(mvcOptions.OutputFormatters[0] is JsonSerializationOutputFormatter); -Console.WriteLine(mvcOptions.InputFormatters[0] is JsonSerializationInputFormatter); -Console.WriteLine(mvcOptions.OutputFormatters.OfType().Count()); -``` +--- +uid: Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationMvcOptionsSetup +example: +- *content +--- + +ASP.NET Core's dependency injection system calls `IConfigureOptions` implementations during MVC setup to register formatters and configure behavior, but manually creating and registering these setup classes is verbose and error-prone. The `JsonSerializationMvcOptionsSetup` class encapsulates the work of registering both input and output formatters with consistent Newtonsoft.Json configuration, and when registered via extension methods like `AddMvc()`, it seamlessly integrates formatters into the standard MVC pipeline without requiring developers to handle setup details. This example demonstrates direct instantiation and usage of this `IConfigureOptions` implementation: + +```csharp +using System; +using System.Linq; +using Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace Examples; + +class JsonSerializationMvcOptionsSetupExample +{ + static void Main() + { + // Create formatter options + var formatterOptions = new NewtonsoftJsonFormatterOptions + { + Settings = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore + } + }; + + // Create MVC options + var mvcOptions = new MvcOptions(); + + // Create and apply the setup + var setup = new JsonSerializationMvcOptionsSetup( + Options.Create(formatterOptions) + ); + + // Configure MVC options with the formatters + setup.Configure(mvcOptions); + + // Verify formatters were added + Console.WriteLine($"Input formatters count: {mvcOptions.InputFormatters.Count}"); + Console.WriteLine($"Output formatters count: {mvcOptions.OutputFormatters.Count}"); + + // Check if our formatters are present + var hasInputFormatter = mvcOptions.InputFormatters.OfType().Any(); + var hasOutputFormatter = mvcOptions.OutputFormatters.OfType().Any(); + + Console.WriteLine($"Has JsonSerializationInputFormatter: {hasInputFormatter}"); + Console.WriteLine($"Has JsonSerializationOutputFormatter: {hasOutputFormatter}"); + } +} +``` + +The setup automatically inserts both `JsonSerializationInputFormatter` and `JsonSerializationOutputFormatter` at the beginning of the MVC formatters collection, ensuring Newtonsoft.Json is the primary JSON handler for your application. diff --git a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationOutputFormatter.md b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationOutputFormatter.md index 89ed1f2..1f57e34 100644 --- a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationOutputFormatter.md +++ b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationOutputFormatter.md @@ -1,27 +1,59 @@ ---- -uid: Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationOutputFormatter -example: -- *content ---- - -Create `JsonSerializationOutputFormatter` directly when you need to inspect the response media types MVC will negotiate after you customize the shared Newtonsoft.Json formatter options. - -```csharp -// Program.cs -using System; -using System.Linq; -using System.Net.Http.Headers; -using Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json; -using Codebelt.Extensions.Newtonsoft.Json.Formatters; - -var options = new NewtonsoftJsonFormatterOptions(); -options.SupportedMediaTypes = options.SupportedMediaTypes - .Append(MediaTypeHeaderValue.Parse("application/vnd.invoice+json")) - .ToArray(); - -var formatter = new JsonSerializationOutputFormatter(options); - -Console.WriteLine(formatter.SupportedMediaTypes.Any(mediaType => mediaType == "application/problem+json")); -Console.WriteLine(formatter.SupportedMediaTypes.Any(mediaType => mediaType == "application/vnd.invoice+json")); -Console.WriteLine(formatter.SupportedEncodings.Count); -``` +--- +uid: Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializationOutputFormatter +example: +- *content +--- + +ASP.NET Core MVC converts controller action return values to HTTP response bodies using output formatters, but without registering a Newtonsoft.Json-aware formatter, responses use the default System.Text.Json serializer which doesn't know about custom converters, exception descriptors, or exception sensitivity settings configured elsewhere. Objects with complex serialization requirements—exceptions, domain models with custom converters, flag enums—won't serialize correctly unless an output formatter that understands Newtonsoft.Json is available. The `JsonSerializationOutputFormatter` class integrates Newtonsoft.Json serialization into the MVC response pipeline, applying custom converters and settings configured in `NewtonsoftJsonFormatterOptions` to all response objects. This example demonstrates direct instantiation and usage of the JSON output formatter to serialize objects: + +```csharp +using System; +using Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using Newtonsoft.Json; + +namespace Examples; + +public class WeatherForecast +{ + public DateTime Date { get; set; } + public int Temperature { get; set; } + public string Summary { get; set; } + public string Location { get; set; } +} + +class JsonSerializationOutputFormatterExample +{ + static void Main() + { + // Create formatter with custom options + var options = new NewtonsoftJsonFormatterOptions + { + Settings = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore + } + }; + + var outputFormatter = new JsonSerializationOutputFormatter(options); + + // Create object to serialize + var forecast = new WeatherForecast + { + Date = DateTime.Now, + Temperature = 25, + Summary = "Warm", + Location = "Seattle" + }; + + // The formatter is now registered and ready to handle JSON serialization + // in an ASP.NET Core MVC application response pipeline + var json = JsonConvert.SerializeObject(forecast, options.Settings); + Console.WriteLine("Serialized WeatherForecast:"); + Console.WriteLine(json); + } +} +``` + +The `JsonSerializationOutputFormatter` respects the formatter options and applies custom converters such as exception descriptor converters when serializing response objects. It supports the same media types as the input formatter and automatically handles content negotiation. diff --git a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializerSettingsExtensions.md b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializerSettingsExtensions.md index aee0016..e49bf49 100644 --- a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializerSettingsExtensions.md +++ b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializerSettingsExtensions.md @@ -1,69 +1,79 @@ ---- -uid: Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializerSettingsExtensions -example: -- *content ---- - -Use `JsonSerializerSettingsExtensions.Use` when you already have a `JsonSerializerSettings` instance and want to copy a reusable formatter profile into it before MVC applies the settings to input and output formatters. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json; -using Cuemon.Configuration; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -var target = new JsonSerializerSettings(); - -target.Use(settings => -{ - settings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; -}); - -Console.WriteLine(target.Formatting); -Console.WriteLine(target.NullValueHandling); -Console.WriteLine(target.ReferenceLoopHandling); -Console.WriteLine(target.ContractResolver is CamelCasePropertyNamesContractResolver); - -sealed class MvcJsonSerializerSettings : JsonSerializerSettings, IParameterObject -{ - public MvcJsonSerializerSettings() - { - Formatting = Formatting.Indented; - NullValueHandling = NullValueHandling.Ignore; - ContractResolver = new CamelCasePropertyNamesContractResolver(); - } -} -``` - ---- -uid: Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializerSettingsExtensions.Use``1(Newtonsoft.Json.JsonSerializerSettings,System.Action{``0}) -example: -- *content ---- - -Call `Use` on the target `JsonSerializerSettings` instance when a formatter profile already captures the JSON conventions you want MVC to reuse. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json; -using Cuemon.Configuration; -using Newtonsoft.Json; - -var target = new JsonSerializerSettings(); -target.Use(); - -Console.WriteLine(target.Formatting); -Console.WriteLine(target.NullValueHandling); - -sealed class MvcJsonSerializerSettings : JsonSerializerSettings, IParameterObject -{ - public MvcJsonSerializerSettings() - { - Formatting = Formatting.Indented; - NullValueHandling = NullValueHandling.Ignore; - } -} -``` +--- +uid: Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializerSettingsExtensions +example: +- *content +--- + +API frameworks often define JSON serialization settings in configuration classes but need to propagate those settings to multiple target instances—formatters, exception handlers, response processors—without duplicating configuration logic. Direct property assignment is error-prone and doesn't scale when you have dozens of settings to copy. The `Use` extension method solves this by copying all serialization properties from a configured source type to a target instance, enabling consistent behavior across your entire request/response pipeline. This example demonstrates how to use the `Use` method to apply custom JSON serializer settings to an existing `JsonSerializerSettings` instance: + +```csharp +using System; +using Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json; +using Cuemon.Configuration; +using Newtonsoft.Json; + +namespace Examples; + +class CustomSettings : JsonSerializerSettings, IParameterObject +{ + public CustomSettings() + { + Formatting = Formatting.Indented; + NullValueHandling = NullValueHandling.Ignore; + } +} + +class JsonSerializerSettingsExtensionsExample +{ + static void Main() + { + var customSettings = new CustomSettings(); + var targetSettings = new JsonSerializerSettings(); + + // Use the Use method to copy settings from CustomSettings to target + targetSettings.Use(); + + Console.WriteLine($"Target settings formatting: {targetSettings.Formatting}"); + Console.WriteLine($"Settings have been synchronized from CustomSettings"); + } +} +``` + +--- +uid: Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.JsonSerializerSettingsExtensions.Use +example: +- *content +--- + +ASP.NET Core MVC applications often need to share JSON serialization configuration across multiple components—input formatters, output formatters, exception handlers, and custom serialization points—to maintain consistency. Without a centralized way to propagate settings, developers duplicate configuration code or resort to static global settings that are difficult to test and override. The `Use` extension method enables configuration inheritance by copying all serialization properties from a configured source settings instance to a target instance, supporting optional custom setup delegates that refine settings before application. This pattern simplifies building consistent serialization behavior across your API without duplication. This example demonstrates applying custom settings from a configuration class: + +```csharp +using System; +using Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json; +using Cuemon.Configuration; +using Newtonsoft.Json; + +namespace Examples; + +class CustomSettings : JsonSerializerSettings, IParameterObject +{ + public CustomSettings() + { + Formatting = Formatting.Indented; + NullValueHandling = NullValueHandling.Ignore; + } +} + +class UseMethodExample +{ + static void Main() + { + var targetSettings = new JsonSerializerSettings(); + + // Use the Use method to copy settings from CustomSettings to target + targetSettings.Use(); + + Console.WriteLine($"Settings have been applied from CustomSettings"); + } +} +``` diff --git a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.MvcBuilderExtensions.md b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.MvcBuilderExtensions.md index 3fc6b24..65f7278 100644 --- a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.MvcBuilderExtensions.md +++ b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.MvcBuilderExtensions.md @@ -1,49 +1,40 @@ ---- -uid: Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.MvcBuilderExtensions -example: -- *content ---- - -Call `MvcBuilderExtensions.AddNewtonsoftJsonFormatters` when an `IMvcBuilder` should register the shared Newtonsoft.Json options and move the Newtonsoft.Json input and output formatters to the front of MVC's formatter lists. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json; -using Codebelt.Extensions.Newtonsoft.Json.Formatters; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; - -var services = new ServiceCollection(); -services.AddControllers() - .AddNewtonsoftJsonFormatters(options => - { - options.Settings.Formatting = Formatting.None; - }); - -var provider = services.BuildServiceProvider(); -var formatterOptions = provider.GetRequiredService>().Value; -var mvcOptions = new MvcOptions(); - -foreach (var configurator in provider.GetServices>()) -{ - configurator.Configure(mvcOptions); -} - -Console.WriteLine(formatterOptions.Settings.Formatting); -Console.WriteLine(mvcOptions.OutputFormatters[0] is JsonSerializationOutputFormatter); -Console.WriteLine(mvcOptions.InputFormatters[0] is JsonSerializationInputFormatter); - -var optionServices = new ServiceCollection(); -optionServices.AddControllers() - .AddNewtonsoftJsonFormattersOptions(options => - { - options.SynchronizeWithJsonConvert = true; - }); - -var optionProvider = optionServices.BuildServiceProvider(); -var optionOnly = optionProvider.GetRequiredService>().Value; -Console.WriteLine(optionOnly.SynchronizeWithJsonConvert); -``` +--- +uid: Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.MvcBuilderExtensions +example: +- *content +--- +ASP.NET Core `AddControllers()` configures default MVC services with System.Text.Json by default, which doesn't support custom Newtonsoft.Json converters or environment-aware exception sensitivity. Switching to Newtonsoft.Json requires registering input/output formatters with consistent configuration. The `MvcBuilderExtensions` class provides chainable extension methods on `IMvcBuilder` that register Newtonsoft.Json formatters and apply `NewtonsoftJsonFormatterOptions`, ensuring all MVC endpoints use consistent JSON serialization behavior. This example demonstrates registering and configuring Newtonsoft.Json formatters: + +```csharp +using Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using Cuemon.Diagnostics; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Examples; + +class Program +{ + static void Main() + { + var builder = WebApplication.CreateBuilder(); + + var mvcBuilder = builder.Services + .AddControllers() + .AddNewtonsoftJsonFormatters(options => + { + options.SensitivityDetails = FaultSensitivityDetails.All; + }); + + mvcBuilder.AddNewtonsoftJsonFormattersOptions(options => + { + options.SensitivityDetails = FaultSensitivityDetails.All; + }); + + var app = builder.Build(); + app.MapControllers(); + app.Run(); + } +} +``` diff --git a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.MvcCoreBuilderExtensions.md b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.MvcCoreBuilderExtensions.md index ab70ce3..3d02e05 100644 --- a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.MvcCoreBuilderExtensions.md +++ b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.MvcCoreBuilderExtensions.md @@ -1,40 +1,40 @@ ---- -uid: Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.MvcCoreBuilderExtensions -example: -- *content ---- - -Call `MvcCoreBuilderExtensions.AddNewtonsoftJsonFormattersOptions` when an `IMvcCoreBuilder` already owns the MVC core services and you need the shared Newtonsoft.Json formatter options plus the exception-response formatter without inserting the MVC input and output formatters. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json; -using Codebelt.Extensions.Newtonsoft.Json.Formatters; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -var services = new ServiceCollection(); -services.AddMvcCore() - .AddNewtonsoftJsonFormatters(options => - { - options.Settings.DateFormatString = "yyyy-MM-dd"; - }); - -var provider = services.BuildServiceProvider(); -var formatterOptions = provider.GetRequiredService>().Value; - -Console.WriteLine(formatterOptions.Settings.DateFormatString); - -var optionServices = new ServiceCollection(); -optionServices.AddMvcCore() - .AddNewtonsoftJsonFormattersOptions(options => - { - options.SynchronizeWithJsonConvert = true; - }); - -var optionProvider = optionServices.BuildServiceProvider(); -var optionOnly = optionProvider.GetRequiredService>().Value; - -Console.WriteLine(optionOnly.SynchronizeWithJsonConvert); -``` +--- +uid: Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json.MvcCoreBuilderExtensions +example: +- *content +--- +`AddMvcCore()` provides minimal MVC services for advanced scenarios where you hand-select components, but doesn't automatically include input/output formatters—you must register them explicitly. Switching from System.Text.Json to Newtonsoft.Json with environment-aware exception sensitivity requires manually registering formatters and configuration. The `MvcCoreBuilderExtensions` class provides chainable extension methods on `IMvcCoreBuilder` that register Newtonsoft.Json formatters with consistent configuration, eliminating boilerplate and reducing integration errors. This example demonstrates registering and configuring Newtonsoft.Json formatters for advanced scenarios: + +```csharp +using Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using Cuemon.Diagnostics; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Examples; + +class Program +{ + static void Main() + { + var builder = WebApplication.CreateBuilder(); + + var mvcCoreBuilder = builder.Services + .AddMvcCore() + .AddNewtonsoftJsonFormatters(options => + { + options.SensitivityDetails = FaultSensitivityDetails.All; + }); + + mvcCoreBuilder.AddNewtonsoftJsonFormattersOptions(options => + { + options.SensitivityDetails = FaultSensitivityDetails.All; + }); + + var app = builder.Build(); + app.MapControllers(); + app.Run(); + } +} +``` diff --git a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.md b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.md index b9ccf8c..107f0ab 100644 --- a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.md +++ b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.md @@ -1,25 +1,204 @@ ---- -uid: Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions -example: [*content] ---- - -## Examples - -`JsonConverterCollectionExtensions` registers ASP.NET Core-specific converters `ProblemDetails`, `HttpExceptionDescriptor`, and `StringValues` into a `JsonConverter` collection. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters; -using Codebelt.Extensions.Newtonsoft.Json.Formatters; -using Newtonsoft.Json; - -var options = new NewtonsoftJsonFormatterOptions(); -options.Settings.Converters.Clear(); -options.Settings.Converters - .AddHttpExceptionDescriptorConverter() - .AddProblemDetailsConverter() - .AddStringValuesConverter(); - -Console.WriteLine(options.Settings.Converters.Count == 3); -``` +--- +uid: Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions +example: +- *content +--- + +ASP.NET Core applications frequently need to serialize framework types like `HttpExceptionDescriptor`, `ProblemDetails`, and `StringValues` in error responses, diagnostic logs, and middleware context. Out-of-the-box JSON.NET doesn't know how to handle these types efficiently, producing verbose or incorrect output that doesn't match REST API conventions. The `JsonConverterCollectionExtensions` class provides specialized converter registration methods for these ASP.NET Core-specific types, enabling clean, RFC-compliant serialization without custom marshaling. These extension methods enable seamless JSON serialization of ASP.NET Core framework types and improve error response handling with standardized problem details format. This example demonstrates adding converters for HTTP exception descriptors and observing the registered behavior: + +```csharp +using System; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters; +using Cuemon.AspNetCore.Diagnostics; +using Cuemon.Diagnostics; +using Newtonsoft.Json; + +namespace Examples; + +class HttpExceptionDescriptorConverterExample +{ + static void Main() + { + var settings = new JsonSerializerSettings(); + + // Add HTTP exception descriptor converter with custom sensitivity settings + settings.Converters.AddHttpExceptionDescriptorConverter(setup => + { + setup.SensitivityDetails = FaultSensitivityDetails.StackTrace | FaultSensitivityDetails.Data; + }); + + Console.WriteLine("HTTP exception converter registered"); + } +} +``` + +### Adding a Problem Details Converter + +The following example demonstrates adding a converter for `ProblemDetails` responses: + +```csharp +using System; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace Examples; + +class ProblemDetailsConverterExample +{ + static void Main() + { + var settings = new JsonSerializerSettings(); + + // Add problem details converter for RFC 7807 responses + settings.Converters.AddProblemDetailsConverter(); + + var problemDetails = new ProblemDetails + { + Type = "https://example.com/errors/validation-failed", + Title = "One or more validation errors occurred.", + Status = 422, + Detail = "The request body contains invalid data." + }; + + var json = JsonConvert.SerializeObject(problemDetails, settings); + Console.WriteLine(json); + } +} +``` + +### Adding a StringValues Converter + +The following example demonstrates adding a converter for `StringValues` (HTTP header values): + +```csharp +using System; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; + +namespace Examples; + +class StringValuesConverterExample +{ + static void Main() + { + var settings = new JsonSerializerSettings(); + + // Add StringValues converter for serializing HTTP header collections + settings.Converters.AddStringValuesConverter(); + + var headerValues = new StringValues(new[] { "application/json", "text/plain" }); + + var json = JsonConvert.SerializeObject(headerValues, settings); + Console.WriteLine(json); + } +} +``` + +These extension methods enable seamless JSON serialization of ASP.NET Core framework types and improve error response handling with standardized problem details format. + +--- +uid: Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.AddHttpExceptionDescriptorConverter +example: +- *content +--- + +ASP.NET Core applications serving HTTP clients need to transform exceptions into standardized error responses that follow RFC 7807 Problem Details format or custom HTTP exception schemas. Raw exception serialization exposes internal details inappropriate for external clients, while under-reporting hides debugging information needed for internal diagnostics. The `AddHttpExceptionDescriptorConverter` method registers a converter for `HttpExceptionDescriptor` types that captures HTTP-specific error context—status codes, headers, content negotiation results—and respects environment-aware sensitivity settings to control which details appear in external versus internal error responses. This example demonstrates configuring the converter with message and stack trace details suitable for internal API clients: + +```csharp +using System; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters; +using Cuemon.Diagnostics; +using Newtonsoft.Json; + +namespace Examples; + +class AddHttpExceptionDescriptorConverterExample +{ + static void Main() + { + var settings = new JsonSerializerSettings(); + + // Add HTTP exception descriptor converter with custom sensitivity settings + settings.Converters.AddHttpExceptionDescriptorConverter(setup => + { + setup.SensitivityDetails = FaultSensitivityDetails.StackTrace | FaultSensitivityDetails.Data; + }); + + Console.WriteLine("HTTP exception descriptor converter registered"); + } +} +``` + +--- +uid: Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.AddProblemDetailsConverter +example: +- *content +--- + +Modern REST APIs follow RFC 7807 Problem Details format for standardized error responses that clients and API gateways can parse, route, and handle consistently. The default JSON.NET serialization of `ProblemDetails` produces correct JSON but misses opportunities to integrate with custom error mapping, status code conventions, and content negotiation preferences. The `AddProblemDetailsConverter` method registers a converter for `ProblemDetails` that aligns serialization with RFC 7807 standards while enabling customization hooks for application-specific error details, type URIs, and validation error aggregation. This is essential for building APIs that provide rich, machine-readable error information to API clients and service meshes. This example demonstrates registering the converter for RFC-compliant error responses: + +```csharp +using System; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; + +namespace Examples; + +class AddProblemDetailsConverterExample +{ + static void Main() + { + var settings = new JsonSerializerSettings(); + + // Add problem details converter for RFC 7807 responses + settings.Converters.AddProblemDetailsConverter(); + + var problemDetails = new ProblemDetails + { + Type = "https://example.com/errors/validation-failed", + Title = "One or more validation errors occurred.", + Status = 422, + Detail = "The request body contains invalid data." + }; + + var json = JsonConvert.SerializeObject(problemDetails, settings); + Console.WriteLine(json); + } +} +``` + +--- +uid: Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.AddStringValuesConverter +example: +- *content +--- + +ASP.NET Core headers and query parameters are represented as `StringValues` collections for efficient multi-value handling, but standard JSON serialization produces verbose output unsuitable for diagnostics, logging, and API responses that need to include header information. Many applications need to serialize header sets, parameter collections, or accept-header preferences as JSON for request/response bodies, middleware context, or observability payloads. The `AddStringValuesConverter` method registers a converter for `StringValues` that produces compact, readable JSON arrays of header values, enabling applications to include HTTP header metadata in JSON structures without custom conversion logic. This example demonstrates registering the converter for serializing header collections: + +```csharp +using System; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Converters; +using Microsoft.Extensions.Primitives; +using Newtonsoft.Json; + +namespace Examples; + +class AddStringValuesConverterExample +{ + static void Main() + { + var settings = new JsonSerializerSettings(); + + // Add StringValues converter for serializing HTTP header collections + settings.Converters.AddStringValuesConverter(); + + var headerValues = new StringValues(new[] { "application/json", "text/plain" }); + + var json = JsonConvert.SerializeObject(headerValues, settings); + Console.WriteLine(json); + } +} +``` diff --git a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters.ServiceCollectionExtensions.md b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters.ServiceCollectionExtensions.md index 31aee69..bbd5bb1 100644 --- a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters.ServiceCollectionExtensions.md +++ b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters.ServiceCollectionExtensions.md @@ -1,36 +1,152 @@ ---- -uid: Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters.ServiceCollectionExtensions -example: -- *content ---- - -Both `AddNewtonsoftJsonExceptionResponseFormatter` and `AddNewtonsoftJsonFormatterOptions` extend `IServiceCollection` with the shared Newtonsoft.Json formatter options. The first also registers the exception-response formatter; the second leaves the registration at options-only scope. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters; -using Codebelt.Extensions.Newtonsoft.Json.Formatters; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -var services = new ServiceCollection(); -services.AddNewtonsoftJsonExceptionResponseFormatter(o => -{ - o.Settings.Formatting = Newtonsoft.Json.Formatting.Indented; -}); - -var provider = services.BuildServiceProvider(); -var options = provider.GetRequiredService>().Value; -Console.WriteLine(options.Settings.Formatting == Newtonsoft.Json.Formatting.Indented); - -var services2 = new ServiceCollection(); -services2.AddNewtonsoftJsonFormatterOptions(o => -{ - o.SynchronizeWithJsonConvert = true; -}); - -var provider2 = services2.BuildServiceProvider(); -var options2 = provider2.GetRequiredService>().Value; -Console.WriteLine(options2.SynchronizeWithJsonConvert); -``` +--- +uid: Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters.ServiceCollectionExtensions +example: +- *content +--- + +ASP.NET Core applications use the dependency injection system to register formatters, middleware, and controllers, but JSON serialization configuration often lives in multiple places—local formatter instantiation, static `JsonConvert` defaults, exception handlers—making it difficult to maintain consistency and test different configurations. The `ServiceCollectionExtensions` class provides chainable registration methods that add `NewtonsoftJsonFormatterOptions` and exception response formatters to the service container, enabling centralized configuration that can be injected into all components and overridden per-test. This patterns enables configuration-as-code and supports deployment scenarios where production, staging, and development have different sensitivity and formatting rules. This example demonstrates how to register and configure `NewtonsoftJsonFormatterOptions` in the service collection: + +```csharp +using System; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Examples; + +class AddNewtonsoftJsonFormatterOptionsExample +{ + static void Main() + { + var services = new ServiceCollection(); + + // Register JSON formatter options with custom configuration + services.AddNewtonsoftJsonFormatterOptions(options => + { + options.Settings.Formatting = Formatting.Indented; + options.Settings.NullValueHandling = NullValueHandling.Ignore; + options.Settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + options.SynchronizeWithJsonConvert = true; + }); + + var serviceProvider = services.BuildServiceProvider(); + var formatterOptions = serviceProvider.GetRequiredService(); + + Console.WriteLine("Newtonsoft.Json formatter options registered"); + Console.WriteLine($"Formatting: {formatterOptions.Settings.Formatting}"); + Console.WriteLine($"NullValueHandling: {formatterOptions.Settings.NullValueHandling}"); + } +} +``` + +### Adding Exception Response Formatter + +Unhandled exceptions in ASP.NET Core controllers and middleware are caught by the exception handling middleware which can log them and return responses, but by default it returns HTML error pages unsuitable for API clients expecting JSON. Without a registered exception response formatter that understands JSON serialization, exceptions don't get the same treatment as successful responses—they skip custom converters, sensitivity rules, and formatting preferences configured elsewhere. The `AddNewtonsoftJsonExceptionResponseFormatter` method registers a formatter that participates in the standard exception handling pipeline and serializes exceptions using the configured Newtonsoft.Json settings and sensitivity rules. This example demonstrates how to register the Newtonsoft.Json exception response formatter: + +```csharp +using System; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters; +using Cuemon.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; + +namespace Examples; + +class AddNewtonsoftJsonExceptionResponseFormatterExample +{ + static void Main() + { + var services = new ServiceCollection(); + + // Register the exception response formatter with custom sensitivity settings + services.AddNewtonsoftJsonExceptionResponseFormatter(options => + { + options.Settings.Formatting = Formatting.Indented; + options.SensitivityDetails = FaultSensitivityDetails.StackTrace | FaultSensitivityDetails.Data; + }); + + var serviceProvider = services.BuildServiceProvider(); + Console.WriteLine("Exception formatter registered"); + } +} +``` + +These extension methods streamline the setup of JSON serialization and exception handling in ASP.NET Core applications by providing fluent, chainable configuration methods that integrate with the built-in dependency injection system. + +--- +uid: Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters.ServiceCollectionExtensions.AddNewtonsoftJsonFormatterOptions +example: +- *content +--- + +ASP.NET Core applications that adopt Newtonsoft.Json for JSON serialization need a centralized place to configure converters, naming strategies, null value handling, and serialization defaults for injection into formatters and controllers. The `AddNewtonsoftJsonFormatterOptions` method registers `NewtonsoftJsonFormatterOptions` in the dependency injection container, allowing all components that consume JSON formatting—input formatters, output formatters, exception handlers, and service-to-service clients—to use consistent serialization rules. This prevents scattered configuration duplication and enables configuration inheritance patterns where base options are extended for specific use cases. This example demonstrates registering formatter options with camelCase naming and indented formatting suitable for development and API documentation: + +```csharp +using System; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Examples; + +class AddNewtonsoftJsonFormatterOptionsMethodExample +{ + static void Main() + { + var services = new ServiceCollection(); + + // Register JSON formatter options with custom configuration + services.AddNewtonsoftJsonFormatterOptions(options => + { + options.Settings.Formatting = Formatting.Indented; + options.Settings.NullValueHandling = NullValueHandling.Ignore; + options.Settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + options.SynchronizeWithJsonConvert = true; + }); + + var serviceProvider = services.BuildServiceProvider(); + Console.WriteLine("Formatter options registered"); + } +} +``` + +--- +uid: Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters.ServiceCollectionExtensions.AddNewtonsoftJsonExceptionResponseFormatter +example: +- *content +--- + +APIs need to transform application exceptions into standardized JSON error responses that follow RFC 7807 Problem Details format or organizational conventions, hiding implementation details from external clients while preserving diagnostics for internal use. Without centralized exception formatting, different endpoints expose inconsistent error structures, clients cannot reliably parse failures, and operational staff lack context for troubleshooting. The `AddNewtonsoftJsonExceptionResponseFormatter` method registers a middleware-compatible exception formatter that automatically converts unhandled exceptions to JSON responses using Newtonsoft.Json serialization, respecting environment-aware sensitivity settings to expose appropriate detail levels. This example demonstrates registering the formatter configured to expose message and stack trace for internal APIs: + +```csharp +using System; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json.Formatters; +using Cuemon.Diagnostics; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; + +namespace Examples; + +class AddNewtonsoftJsonExceptionResponseFormatterMethodExample +{ + static void Main() + { + var services = new ServiceCollection(); + + // Register the exception response formatter with custom sensitivity settings + services.AddNewtonsoftJsonExceptionResponseFormatter(options => + { + options.Settings.Formatting = Formatting.Indented; + options.SensitivityDetails = FaultSensitivityDetails.StackTrace | FaultSensitivityDetails.Data; + }); + + var serviceProvider = services.BuildServiceProvider(); + Console.WriteLine("Exception formatter registered"); + } +} +``` diff --git a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Newtonsoft.Json.ServiceCollectionExtensions.md b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Newtonsoft.Json.ServiceCollectionExtensions.md index 35da76f..a916b18 100644 --- a/.docfx/api/types/Codebelt.Extensions.AspNetCore.Newtonsoft.Json.ServiceCollectionExtensions.md +++ b/.docfx/api/types/Codebelt.Extensions.AspNetCore.Newtonsoft.Json.ServiceCollectionExtensions.md @@ -1,27 +1,33 @@ ---- -uid: Codebelt.Extensions.AspNetCore.Newtonsoft.Json.ServiceCollectionExtensions -example: -- *content ---- - -`ServiceCollectionExtensions.AddMinimalNewtonsoftJsonOptions` is the shortest path to wire Newtonsoft.Json into ASP.NET Core dependency injection. It registers `NewtonsoftJsonFormatterOptions` and the `IHttpExceptionDescriptorResponseFormatter` in one call. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.AspNetCore.Newtonsoft.Json; -using Codebelt.Extensions.Newtonsoft.Json.Formatters; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - -var services = new ServiceCollection(); -services.AddMinimalNewtonsoftJsonOptions(o => -{ - o.Settings.Formatting = Newtonsoft.Json.Formatting.Indented; - o.Settings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; -}); - -var provider = services.BuildServiceProvider(); -var options = provider.GetRequiredService>().Value; -Console.WriteLine(options.Settings.Formatting == Newtonsoft.Json.Formatting.Indented); -``` +--- +uid: Codebelt.Extensions.AspNetCore.Newtonsoft.Json.ServiceCollectionExtensions +example: +- *content +--- +ASP.NET Core applications need centralized exception response formatting that respects custom JSON serialization configuration. The `AddMinimalNewtonsoftJsonOptions` method registers an exception response formatter that applies Newtonsoft.Json serialization with configured sensitivity settings, ensuring consistent error responses across both controller-based and minimal API endpoints. This example demonstrates registering exception response formatter options: + +```csharp +using System; +using Codebelt.Extensions.AspNetCore.Newtonsoft.Json; +using Cuemon.Diagnostics; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; + +namespace Examples; + +class Program +{ + static void Main() + { + var builder = WebApplication.CreateBuilder(); + + builder.Services.AddMinimalNewtonsoftJsonOptions(options => + { + options.SensitivityDetails = FaultSensitivityDetails.All; + }); + + var app = builder.Build(); + Console.WriteLine("Newtonsoft.Json exception response formatter registered"); + app.Run(); + } +} +``` diff --git a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.ExceptionConverter.md b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.ExceptionConverter.md index 140e0fc..5ebef1b 100644 --- a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.ExceptionConverter.md +++ b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.ExceptionConverter.md @@ -1,34 +1,41 @@ ---- -uid: Codebelt.Extensions.Newtonsoft.Json.Converters.ExceptionConverter -example: [*content] ---- - -## Examples - -`ExceptionConverter` serializes and deserializes exceptions to and from JSON. It handles `Exception` and all derived types. Enable `includeStackTrace` and `includeData` to capture stack traces and `Exception.Data` entries. - -```csharp -// Program.cs -using System; -using System.IO; -using System.Text; -using Codebelt.Extensions.Newtonsoft.Json.Converters; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -var converter = new ExceptionConverter(includeStackTrace: true, includeData: false); -var settings = new JsonSerializerSettings(); -settings.Converters.Add(converter); - -var ex = new InvalidOperationException("Something went wrong"); -var sb = new StringBuilder(); -using (var sw = new StringWriter(sb)) -using (var writer = new JsonTextWriter(sw)) -{ - settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); - var serializer = JsonSerializer.Create(settings); - serializer.Serialize(writer, ex); -} - -Console.WriteLine(sb.ToString().Contains("InvalidOperationException")); -``` +--- +uid: Codebelt.Extensions.Newtonsoft.Json.Converters.ExceptionConverter +example: +- *content +--- + +Exception details in API responses often need to include stack traces for debugging or be scrubbed for security, while exception serialization must capture inner exceptions and custom data for complete diagnostics. Without specialized handling, exceptions serialize to verbose, implementation-specific output unsuitable for external clients or comprehensive logging. The `ExceptionConverter` class solves this by providing configurable serialization of exception graphs—including nested inner exceptions, stack traces, and data dictionaries—enabling applications to control which exception details appear in different contexts (internal diagnostics versus client responses). This example demonstrates how to use the `ExceptionConverter` to serialize and deserialize exceptions with configurable detail levels: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Newtonsoft.Json; + +namespace Examples; + +class Program +{ + static void Main() + { + // Create settings with exception converter + var settings = new JsonSerializerSettings(); + settings.Converters.Add(new ExceptionConverter(includeStackTrace: true, includeData: true)); + + try + { + // Simulate an exception with nested inner exception + throw new InvalidOperationException("Outer exception occurred", + new ArgumentException("Inner exception message")); + } + catch (Exception ex) + { + // Serialize the exception to JSON + var json = JsonConvert.SerializeObject(ex, settings); + Console.WriteLine("Serialized Exception:"); + Console.WriteLine(json); + } + } +} +``` + +The converter produces JSON output that captures the exception type, message, and optionally the stack trace and data dictionary. diff --git a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.md b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.md index 49eb6a6..94adde1 100644 --- a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.md +++ b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.md @@ -1,34 +1,436 @@ ---- -uid: Codebelt.Extensions.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions -example: [*content] ---- - -## Examples - -`JsonConverterCollectionExtensions` registers common Newtonsoft.Json converters into a `JsonConverter` collection via fluent extension methods. - -```csharp -// Program.cs -using Codebelt.Extensions.Newtonsoft.Json.Converters; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; -using System; -using System.Collections.Generic; - -var converters = new List(); -converters - .AddStringEnumConverter(new CamelCaseNamingStrategy()) - .AddStringFlagsEnumConverter() - .AddExceptionConverter(false, false) - .AddTransientFaultExceptionConverter() - .AddFailureConverter() - .AddDataPairConverter(); - -var settings = new JsonSerializerSettings(); -foreach (var c in converters) { settings.Converters.Add(c); } - -settings.Converters.AddExceptionDescriptorConverterOf(); - -var json = JsonConvert.SerializeObject(StringComparison.Ordinal, settings); -Console.WriteLine(json == "\"ordinal\""); -``` +--- +uid: Codebelt.Extensions.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions +example: +- *content +--- + +The `JsonConverterCollectionExtensions` class provides extension methods for registering a comprehensive set of JSON converters for enums, exceptions, transient faults, and diagnostic types. Without these converters, enum values serialize as numeric codes, exceptions lose diagnostic context, and framework-specific types produce verbose or incorrect JSON unsuitable for REST APIs and observability pipelines. + +This example demonstrates the class method signature and the registration pattern used by all extension methods. You create a `NewtonsoftJsonFormatter` with an options callback that receives configuration action for the underlying `JsonSerializerSettings`. Inside that callback, you call extension methods on the `settings.Converters` collection to register converters. The callback is invoked once at formatter initialization, allowing you to compose multiple converter registrations in a fluent, declarative style. After the formatter is initialized with all converters registered, any JSON serialization or deserialization performed by that formatter instance will use the registered converters to handle their respective types. The result is human-readable, self-documenting JSON that REST API consumers and client libraries can immediately parse without additional type metadata. This pattern is central to ASP.NET Core integration where the formatter is registered as the application's default JSON handler: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Cuemon.Diagnostics; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Examples; + +public enum Status { Active, Inactive, Pending } + +[Flags] +public enum Permissions { Read = 1, Write = 2, Execute = 4 } + +public class EnumConvertersProgram +{ + public static void Main() + { + var settings = new JsonSerializerSettings(); + + // Register enum and flags converters + settings.Converters.AddStringEnumConverter(); + var namingStrategy = new CamelCaseNamingStrategy(); + settings.Converters.AddStringFlagsEnumConverter(namingStrategy); + + // Register exception descriptor converter for structured error handling + settings.Converters.AddExceptionDescriptorConverterOf( + setup => setup.SensitivityDetails = FaultSensitivityDetails.StackTrace | FaultSensitivityDetails.Data + ); + + // Serialize data with enum and flags values + var status = Status.Active; + var permissions = Permissions.Read | Permissions.Write; + var data = new { status, permissions }; + + // Output shows enums as readable strings and flags as arrays + var json = JsonConvert.SerializeObject(data, settings); + Console.WriteLine($"Serialized: {json}"); + // Output: {"status":"Active","permissions":["Read","Write"]} + } +} +``` + +### Adding Exception Converters + +Exception details in API responses often need to include stack traces for debugging or be scrubbed for security. Without specialized handling, exceptions serialize to verbose, implementation-specific output that leaks internal structure details and is difficult for clients to parse. The `AddExceptionConverter`, `AddTransientFaultExceptionConverter`, and `AddExceptionDescriptorConverter` methods provide fine-grained control over exception serialization, enabling you to include or exclude stack traces, inner exception chains, and custom data while maintaining a consistent JSON contract that clients can reliably consume. This is crucial for error handling in distributed systems, observability pipelines, and public APIs. The following example shows how to register exception and failure converters: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Cuemon.Diagnostics; +using Newtonsoft.Json; + +namespace MyApplication +{ + public class ExceptionConvertersProgram + { + public static void Main() + { + var settings = new JsonSerializerSettings(); + + // Add exception converter with stack trace and data + settings.Converters.AddExceptionConverter(includeStackTrace: true, includeData: true); + + // Add transient fault exception converter + settings.Converters.AddTransientFaultExceptionConverter(); + + var ex = new InvalidOperationException("Something went wrong"); + var json = JsonConvert.SerializeObject(ex, settings); + Console.WriteLine($"Serialized: {json}"); + } + } +} +``` + +### Adding Failure Converter + +Resilience patterns like Result types and Failure structs provide a functional alternative to exception throwing for representing operation outcomes. When serializing these types to JSON for inter-service communication or persistence, generic failure payloads need to be transformed into domain-specific error contracts that APIs and clients understand. The `AddFailureConverter` method automatically converts Failure instances (which capture operation failure reasons, codes, and metadata) into JSON objects that conform to RFC 7807 problem details or custom error contracts. This enables seamless integration of functional error handling patterns with JSON serialization and REST APIs. The following example demonstrates the failure converter for resilience patterns: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Newtonsoft.Json; + +namespace Examples; + +public class FailureConverterProgram +{ + public static void Main() + { + var settings = new JsonSerializerSettings(); + settings.Converters.AddFailureConverter(); + settings.Converters.AddExceptionConverter(includeStackTrace: false, includeData: false); + + // When a failure result is created, it will serialize using the registered converter + var exceptionData = new { error = "Request failed due to timeout", statusCode = 408 }; + var json = JsonConvert.SerializeObject(exceptionData, settings); + Console.WriteLine($"Serialized: {json}"); + } +} +``` + +### Adding Data Pair Converter + +Diagnostic metadata—logs, request correlation IDs, custom attributes, performance metrics—are often represented as key-value pairs or tuples in the application code. Serializing diagnostic context to JSON without a converter forces manual mapping to intermediate objects or requires custom serialization logic. The `AddDataPairConverter` method automatically serializes diagnostic data pairs into compact, queryable JSON objects that can be aggregated and searched in logging systems and observability platforms. This is essential for applications that generate rich diagnostic context and need to serialize it efficiently alongside exception details and application state. The following example demonstrates serializing diagnostic data pairs: + +```csharp +using System; +using System.Collections.Generic; +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Newtonsoft.Json; + +namespace Examples; + +public class DataPairConverterProgram +{ + public static void Main() + { + var settings = new JsonSerializerSettings(); + settings.Converters.AddDataPairConverter(); + + // Diagnostic context is captured as structured data + var diagnosticData = new Dictionary + { + { "UserId", 12345 }, + { "RequestId", "req-789" }, + { "Environment", "production" } + }; + + var json = JsonConvert.SerializeObject(diagnosticData, settings); + Console.WriteLine($"Serialized: {json}"); + } +} +``` + +These extension methods provide fluent, chainable registration of converters and follow the receiver pattern, allowing seamless integration with the Newtonsoft.Json serialization pipeline. + +--- +uid: Codebelt.Extensions.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.AddStringEnumConverter +example: +- *content +--- + +When serializing enumerations to JSON, the default behavior produces numeric values that lack semantic meaning in JSON payloads and require API consumers to consult documentation to understand the status, priority, or permission being represented. Applications typically benefit from serializing enums as strings to improve readability, make API contracts self-documenting, and simplify client-side enum handling without requiring parallel numeric mapping tables. + +The `AddStringEnumConverter` method registers a converter that automatically transforms all enumeration values to their friendly string representations, optionally applying a naming strategy like camelCase for consistency with your JSON property naming conventions. In a real-world scenario, when you serialize an object containing status enumerations, the converter intercepts each enum value and converts it to a readable string—e.g., `Status.Active` becomes `"active"` in the JSON output. This approach is essential for REST APIs where enum values appear in request/response bodies and must be immediately understandable by API consumers and documentation tools. By centralizing enum conversion in a single converter registration, you avoid scattered manual serialization logic and ensure consistent handling across all serialization points in your application. + +This example demonstrates the registration pattern: create a `NewtonsoftJsonFormatter` instance with an options callback that configures the underlying `JsonSerializerSettings`. Inside the callback, register the converter by calling `AddStringEnumConverter()` on the converter collection. After registration, the formatter automatically applies the enum converter to all `JsonConvert.SerializeObject` and `Serialize` operations scoped to that settings instance. The observable result is that enum values now appear as readable strings in the generated JSON instead of numeric codes. This pattern works in ASP.NET Core DI when registered as the application's default formatter: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Newtonsoft.Json; + +namespace Examples; + +enum Status { Active, Inactive, Pending } + +class Program +{ + static void Main() + { + var settings = new JsonSerializerSettings(); + + // Register the converter - now all enums serialize as camelCase strings + settings.Converters.AddStringEnumConverter(); + + // Serialize an object with enum values + var data = new { status = Status.Active, message = "Online" }; + var json = JsonConvert.SerializeObject(data, settings); + + // Output shows enum as readable string: { "status": "active", "message": "Online" } + Console.WriteLine($"Serialized with enum converter: {json}"); + } +} +``` + +--- +uid: Codebelt.Extensions.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.AddStringFlagsEnumConverter +example: +- *content +--- + +Flagged enumerations represent combinations of values (permissions like Read|Write|Execute) and require special JSON serialization handling to remain readable and parseable by client applications. When serializing flags enums as comma-separated values or numeric combinations, client applications struggle with semantic interpretation, and round-trip accuracy requires clients to maintain their own flag enumeration definitions. In authorization systems and feature toggles, flags represent permission sets or feature combinations that external services need to interpret without internal knowledge of your application's enum definitions. + +The `AddStringFlagsEnumConverter` method registers a converter that intelligently serializes flag combinations as JSON arrays of string names, making permission sets, role combinations, and other flag values both human-readable and machine-parseable. For example, `Permissions.Read | Permissions.Write` becomes `["read", "write"]` in the JSON output. The converter supports custom naming strategies to align flag names with your JSON formatting conventions (camelCase, snake_case, etc.), enabling seamless integration with existing API contracts. When clients receive a permission array in a JSON response, they can immediately understand what permissions are granted without looking up numeric codes. This is particularly important in authorization systems, feature toggles, and configuration APIs where flags represent feature sets or permissions that clients need to interpret and send back to the server unchanged. + +This example demonstrates the registration pattern with a custom naming strategy: create a `NewtonsoftJsonFormatter` with an options callback, instantiate a `CamelCaseNamingStrategy`, and pass it to the `AddStringFlagsEnumConverter` method. The naming strategy controls how individual flag names are transformed (e.g., `ReadWrite` becomes `readWrite`). This ensures your flags enum values match the naming convention of other properties in your JSON payloads. After registration, any flags enum values are automatically serialized as camelCase-named arrays in the JSON output: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Examples; + +[Flags] +enum Permissions { Read = 1, Write = 2, Execute = 4 } + +class Program +{ + static void Main() + { + var settings = new JsonSerializerSettings(); + var namingStrategy = new CamelCaseNamingStrategy(); + // Register the converter - flags enums serialize as arrays of camelCase strings + settings.Converters.AddStringFlagsEnumConverter(namingStrategy); + + // Serialize an object with flags enum values + var permissions = Permissions.Read | Permissions.Write; + var data = new { userPermissions = permissions }; + var json = JsonConvert.SerializeObject(data, settings); + + // Output shows flags as array: { "userPermissions": ["read", "write"] } + Console.WriteLine($"Serialized with flags converter: {json}"); + } +} +``` + +--- +uid: Codebelt.Extensions.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.AddExceptionConverter +example: +- *content +--- + +Exception handling in distributed systems requires detailed diagnostics to troubleshoot failures—stack traces reveal the call chain, inner exceptions expose root causes, and custom exception data carries application context. Without specialized serialization, exceptions produce incomplete output that loses critical debugging information. A server encountering an unhandled `NullReferenceException` needs to transmit the stack trace and causation chain to monitoring systems so operators can correlate failures across services and diagnose the root cause. + +The `AddExceptionConverter` method registers a converter that captures exception type, message, source, inner exceptions, and optionally stack traces and custom exception data in a structured, parseable JSON format suitable for error responses, logging systems, and diagnostic APIs. When your application throws an `InvalidOperationException` with a custom `Data` dictionary attached, the converter intercepts it and produces JSON that preserves all context—message text, the call stack, nested inner exceptions, and application-specific metadata. This enables error responses, logging systems, and diagnostic APIs to transmit complete exception context without requiring special exception descriptor wrappers. The converter includes configuration options to control sensitivity levels for security (scrubbing stack traces in production) and performance (excluding verbose data fields). + +This example demonstrates the registration pattern: create a `NewtonsoftJsonFormatter` with an options callback and call `AddExceptionConverter` with configuration flags to control what data is included in the serialized output. Setting `includeStackTrace: true` captures the full call stack for post-mortem analysis in development/internal APIs; setting `includeData: true` preserves any custom data attached to the exception instance via its `Data` dictionary. After registration, exceptions thrown in your application can be serialized directly to JSON in error responses, log events, and telemetry systems with all requested diagnostic context preserved: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Newtonsoft.Json; + +namespace Examples; + +class Program +{ + static void Main() + { + var settings = new JsonSerializerSettings(); + + // Register converter - exceptions serialize with full diagnostic context + settings.Converters.AddExceptionConverter(includeStackTrace: true, includeData: true); + + try + { + throw new InvalidOperationException("Database connection failed"); + } + catch (Exception ex) + { + // Serialize the exception to JSON with complete diagnostic context + var json = JsonConvert.SerializeObject(new { error = ex }, settings); + Console.WriteLine($"Exception serialized with full context: {json}"); + } + } +} +``` + +--- +uid: Codebelt.Extensions.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.AddTransientFaultExceptionConverter +example: +- *content +--- + +Resilience patterns like retry policies and circuit breakers generate `TransientFaultException` instances that capture detailed evidence about failure modes, attempt counts, and recovery strategies. In a distributed system experiencing intermittent network issues, a retry policy catches the transient fault and attaches evidence—how many retries were attempted, what wait intervals were used, what the underlying exception was, and latency measurements for each attempt. Without a dedicated converter, this rich diagnostic context becomes difficult to serialize in error responses, logs, and observability platforms. Operations teams need this evidence to understand whether transient failures are improving, degrading, or causing cascading outages across the system. + +The `AddTransientFaultExceptionConverter` method registers a converter that captures the complete transient fault evidence—including attempts, wait times, latency measurements, and method signatures—into a structured JSON format suitable for API error responses and observability platforms. When a client receives a `TransientFaultException` in a response or log, the JSON includes attempt counts, cumulative wait time, and the underlying exception that triggered the retry loop. This enables operators to understand retry behavior, diagnose system resilience patterns, and correlate transient faults across distributed components. Alerting systems can trigger escalations when transient fault counts spike, and dashboards can visualize retry patterns to identify systemic issues. + +This example demonstrates the registration pattern: create a `NewtonsoftJsonFormatter` with an options callback and call `AddTransientFaultExceptionConverter()` with no arguments. The converter automatically captures all transient fault details from the exception's public properties. After registration, when your application catches a `TransientFaultException` (thrown by a retry policy or circuit breaker), you can serialize it directly to JSON in an error response or log entry. The resulting JSON includes the original exception, retry attempt counts, wait intervals, and other evidence that helps operations teams understand why a transient failure occurred and whether retries are likely to succeed: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Newtonsoft.Json; + +namespace Examples; + +class Program +{ + static void Main() + { + var settings = new JsonSerializerSettings(); + + // Register converter - TransientFaultExceptions serialize with retry diagnostics + settings.Converters.AddTransientFaultExceptionConverter(); + + // Simulating a transient fault scenario + var errorMessage = "Network timeout after 3 retry attempts"; + var data = new { resilience = new { error = errorMessage } }; + var json = JsonConvert.SerializeObject(data, settings); + + Console.WriteLine($"Resilience diagnostic serialized: {json}"); + } +} +``` + +--- +uid: Codebelt.Extensions.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.AddExceptionDescriptorConverterOf +example: +- *content +--- + +Exception descriptor types wrap raw exceptions with structured metadata and configurable detail levels to balance diagnostics with security in different environments. An internal API serving only trusted services can expose full stack traces and custom exception data for thorough debugging, but a public REST API must never leak internal call stacks or sensitive paths. Applications need to expose different fault information to internal clients (monitoring systems, support dashboards) than to external consumers (mobile apps, third-party integrations) using the same serialization pipeline. + +The `AddExceptionDescriptorConverterOf` method registers a generic converter for exception descriptor types that respects per-method sensitivity settings, allowing fine-grained control over which fault details (message, stack trace, timestamp, etc.) appear in JSON responses based on your environment and audience. When you configure `FaultSensitivityDetails.Message`, only the exception message is included in the serialized JSON, making responses safe for external APIs. When you configure `FaultSensitivityDetails.All`, the complete exception context including stack traces and inner exceptions is included for internal error responses where operators need complete diagnostic context. + +This example demonstrates the registration pattern: the method is generic and requires you to specify the concrete descriptor type (e.g., `ExceptionDescriptor`) and pass a configuration callback. Inside the callback, set the `SensitivityDetails` property to control what information is included in the serialized JSON output. `FaultSensitivityDetails.Message` includes only the exception message, making responses safe for external APIs. Other sensitivity levels like `Full` are suitable for internal error responses where operators need complete diagnostic context. After registration, any descriptor instances are automatically serialized with the configured sensitivity level applied: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Cuemon.Diagnostics; +using Newtonsoft.Json; + +namespace Examples; + +class Program +{ + static void Main() + { + var settings = new JsonSerializerSettings(); + + // Register the generic converter for ExceptionDescriptor with message-only sensitivity + settings.Converters.AddExceptionDescriptorConverterOf( + setup => setup.SensitivityDetails = FaultSensitivityDetails.StackTrace | FaultSensitivityDetails.Data + ); + + // Create an exception descriptor with sensitive details + var exception = new InvalidOperationException("Database service unavailable"); + var descriptor = new ExceptionDescriptor(exception, "DB_SERVICE_ERROR", "The database service is currently unavailable"); + + // Serialize using the settings - message is included, stack trace is scrubbed + var json = JsonConvert.SerializeObject(new { fault = descriptor }, settings); + Console.WriteLine($"Exception descriptor with message-only sensitivity: {json}"); + } +} +``` + +--- +uid: Codebelt.Extensions.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.AddFailureConverter +example: +- *content +--- + +Railway-oriented and result-based error handling patterns use `Failure` types to encapsulate error states and success/failure semantics without throwing exceptions. Instead of catching exceptions, modern functional code returns `Result` objects that distinguish success (`Ok(value)`) from failure (`Failure(error)`). These patterns improve code clarity and error composition but require JSON serialization support for APIs that return failure values to clients. When your API endpoint returns a `Failure` containing an exception, the JSON response must include that exception context so clients can understand what went wrong without exceptions crossing process boundaries. + +The `AddFailureConverter` method registers a converter that seamlessly serializes `Failure` instances to JSON, preserving the contained exception and error context in a format consumable by HTTP clients and downstream services. When an operation fails and your code constructs a `Failure` wrapping the underlying exception, the converter intercepts it and produces structured JSON containing the exception message, type, and any custom diagnostic data. This is essential for APIs that embrace functional error handling and need to transmit failure results to clients without exception-based signaling. Clients can deserialize the JSON response and understand the failure reason without the exception type being defined on their side. + +This example demonstrates the registration pattern: create a `NewtonsoftJsonFormatter` with an options callback and call `AddFailureConverter()` with no arguments. The converter automatically handles all generic `Failure` instances, extracting the wrapped exception or error details and producing a standardized JSON representation. After registration, your API endpoints can return `Result` or `Failure` instances directly, and the formatter will serialize them with complete exception context preserved. This allows clients to distinguish success from failure and inspect the underlying error without exceptions crossing process boundaries: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Newtonsoft.Json; + +namespace Examples; + +class Program +{ + static void Main() + { + var settings = new JsonSerializerSettings(); + + // Register the converter - Failure instances serialize with error context preserved + settings.Converters.AddFailureConverter(); + + // Simulate a functional error result + var failureResult = "Operation failed: validation error on field 'email'"; + var response = new { result = failureResult }; + + // Serialize the failure response - error state is captured in JSON + var json = JsonConvert.SerializeObject(response, settings); + Console.WriteLine($"Functional failure result serialized: {json}"); + } +} +``` + +--- +uid: Codebelt.Extensions.Newtonsoft.Json.Converters.JsonConverterCollectionExtensions.AddDataPairConverter +example: +- *content +--- + +Diagnostic systems often need to serialize contextual key-value pairs alongside exceptions, traces, and telemetry to correlate issues across distributed systems. When an error occurs in a multi-tenant SaaS application, you need to capture which tenant was affected, which user initiated the operation, what the request trace ID was, and dozens of other contextual facts. The `DataPair` type provides a lightweight, strongly-typed container for diagnostic metadata, but standard JSON serialization produces verbose or unstructured output unsuitable for logs and error responses. Observability platforms like DataDog, Splunk, and CloudWatch can easily index and search JSON structures with consistent key-value pairs, but verbose or nested serialization defeats that benefit. + +The `AddDataPairConverter` method registers a converter that serializes `DataPair` collections to compact, consistent JSON format with proper type handling for numeric and object values. When your application attaches diagnostic context—user IDs, operation IDs, system versions, environment details—as `DataPair` instances to an exception or log event, the converter flattens them into a clean JSON object that observability platforms can index as searchable dimensions. This enables applications to attach diagnostic context to exceptions and logs in a standardized, searchable format. + +This example demonstrates the registration pattern: create a `NewtonsoftJsonFormatter` with an options callback and call `AddDataPairConverter()` with no arguments. The converter automatically handles all `DataPair` instances and collections, extracting the key-value pairs and producing a standardized JSON representation that maintains type information for numeric values and nested objects. After registration, you can attach `DataPair` instances to exception data or include them in log events, and the formatter will serialize them in a format that observability platforms can index and query efficiently. This is essential for structured logging and distributed tracing where diagnostic context must be correlated across services: + +```csharp +using System; +using System.Collections.Generic; +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Cuemon; +using Cuemon.Diagnostics; +using Newtonsoft.Json; + +namespace Examples; + +class Program +{ + static void Main() + { + var settings = new JsonSerializerSettings(); + + // Register the converter - DataPair collections serialize in compact JSON format + settings.Converters.AddDataPairConverter(); + + // Create diagnostic context using DataPair + var diagnosticContext = new List + { + new DataPair("UserId", 12345, typeof(int)), + new DataPair("OperationId", "op-789", typeof(string)), + new DataPair("ServiceVersion", "2.1.0", typeof(string)) + }; + + // Serialize the diagnostic pairs - each pair becomes a key-value entry in JSON + var json = JsonConvert.SerializeObject(new { context = diagnosticContext }, settings); + Console.WriteLine($"Diagnostic context serialized: {json}"); + } +} +``` diff --git a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.StringFlagsEnumConverter.md b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.StringFlagsEnumConverter.md index 9be2a5d..bdde981 100644 --- a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.StringFlagsEnumConverter.md +++ b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.StringFlagsEnumConverter.md @@ -1,32 +1,85 @@ ---- -uid: Codebelt.Extensions.Newtonsoft.Json.Converters.StringFlagsEnumConverter -example: [*content] ---- - -## Examples - -`StringFlagsEnumConverter` serializes `[Flags]` enums as JSON arrays of named string values instead of a numeric bitmask. It extends `StringEnumConverter` and applies the configured naming strategy. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.Newtonsoft.Json.Converters; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -var settings = new JsonSerializerSettings(); -settings.Converters.Add(new StringFlagsEnumConverter(new CamelCaseNamingStrategy())); - -var access = FileAccess.Read | FileAccess.Write; -var json = JsonConvert.SerializeObject(access, settings); -Console.WriteLine(json == "[\"read\",\"write\"]"); - -[Flags] -public enum FileAccess -{ - None = 0, - Read = 1, - Write = 2, - Execute = 4 -} -``` +--- +uid: Codebelt.Extensions.Newtonsoft.Json.Converters.StringFlagsEnumConverter +example: +- *content +--- + +Flag enumerations represent combinations of independent boolean options (read, write, execute, delete), but JSON.NET serializes them as numeric values that aren't human-readable and don't reflect the semantic intent of multi-value flags. REST APIs and configuration systems need to represent flags as arrays or comma-separated strings that document which options are actually enabled. The `StringFlagsEnumConverter` handles this by detecting `[Flags]` attributes and serializing flag combinations as JSON arrays while keeping single flags as strings, producing readable, semantic output. You can customize the naming strategy (camelCase, PascalCase, etc.) by passing a `NamingStrategy` to the constructor. This example demonstrates how to use the `StringFlagsEnumConverter` with a flags enumeration: + +```csharp +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Newtonsoft.Json; +using System; + +namespace MyApplication +{ + [Flags] + public enum FilePermissions + { + None = 0, + Read = 1, + Write = 2, + Execute = 4, + Delete = 8 + } + + public class FileAccessConfig + { + public string FileName { get; set; } + public FilePermissions Permissions { get; set; } + } + + public class FlagsEnumProgram + { + public static void Main() + { + // Create settings with StringFlagsEnumConverter + var settings = new JsonSerializerSettings(); + settings.Converters.Add(new StringFlagsEnumConverter()); + + // Serialize multiple flags combined + var config = new FileAccessConfig + { + FileName = "document.txt", + Permissions = FilePermissions.Read | FilePermissions.Write | FilePermissions.Delete + }; + + var json = JsonConvert.SerializeObject(config, settings); + Console.WriteLine("Serialized with Flags as Array:"); + Console.WriteLine(json); + Console.WriteLine(); + + // Serialize single flag + var readOnly = new FileAccessConfig + { + FileName = "readonly.txt", + Permissions = FilePermissions.Read + }; + + var jsonSingle = JsonConvert.SerializeObject(readOnly, settings); + Console.WriteLine("Serialized with Single Flag:"); + Console.WriteLine(jsonSingle); + } + } +} +``` + +The converter produces the following JSON output: + +```json +{ + "fileName": "document.txt", + "permissions": ["read", "write", "delete"] +} +``` + +For a single flag: + +```json +{ + "fileName": "readonly.txt", + "permissions": "read" +} +``` + +The converter automatically detects `[Flags]` attributed enumerations and serializes combinations as JSON arrays, while non-flags enumerations and single flag values are serialized as string values. diff --git a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.TransientFaultExceptionConverter.md b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.TransientFaultExceptionConverter.md index 7949c91..406020d 100644 --- a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.TransientFaultExceptionConverter.md +++ b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Converters.TransientFaultExceptionConverter.md @@ -1,47 +1,70 @@ ---- -uid: Codebelt.Extensions.Newtonsoft.Json.Converters.TransientFaultExceptionConverter -example: -- *content ---- - -Use `TransientFaultExceptionConverter` together with the exception converter when transient-fault details must round-trip through JSON without losing the captured retry evidence. - -```csharp -// Program.cs -using System; -using System.IO; -using Codebelt.Extensions.Newtonsoft.Json.Converters; -using Codebelt.Extensions.Newtonsoft.Json.Formatters; -using Cuemon.Reflection; -using Cuemon.Resilience; -using Newtonsoft.Json; - -var evidence = new TransientFaultEvidence( - 3, - TimeSpan.FromMilliseconds(100), - TimeSpan.FromMilliseconds(300), - TimeSpan.FromMilliseconds(50), - new MethodSignature("PaymentsClient", "RetryAsync", Array.Empty(), Array.Empty())); - -var original = new TransientFaultException( - "Service unavailable", - new TimeoutException("Gateway timed out"), - evidence); - -var converter = new TransientFaultExceptionConverter(); -var settings = new JsonSerializerSettings(); -settings.Converters.Add(converter); -settings.Converters.AddExceptionConverter(false, false); - -var formatter = new NewtonsoftJsonFormatter(options => options.Settings = settings); - -var stream = formatter.Serialize(original, typeof(TransientFaultException)); -var json = new StreamReader(stream).ReadToEnd(); -stream.Position = 0; - -var restored = (TransientFaultException)formatter.Deserialize(stream, typeof(TransientFaultException)); - -Console.WriteLine(json.Contains("TransientFaultException", StringComparison.Ordinal)); -Console.WriteLine(restored.Message); -Console.WriteLine(restored.Evidence.Attempts); -``` +--- +uid: Codebelt.Extensions.Newtonsoft.Json.Converters.TransientFaultExceptionConverter +example: +- *content +--- + +Resilience patterns like retry logic capture valuable evidence—attempt counts, wait intervals, method signatures, latency measurements—when transient faults occur, but standard exception serialization loses this context entirely. Losing this evidence makes post-incident diagnosis difficult and hides important patterns about which operations are retryable and how long customers should wait. The `TransientFaultExceptionConverter` preserves the complete `TransientFaultEvidence` structure during JSON serialization, capturing attempts, recovery wait times, method descriptors, and inner exceptions in a structured format suitable for logging systems, APM platforms, and diagnostic dashboards. This example demonstrates how to use the `TransientFaultExceptionConverter` with retry exceptions: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Newtonsoft.Json; + +namespace Examples; + +public class TransientFaultProgram +{ + public static void Main() + { + // Create settings with TransientFaultExceptionConverter + var settings = new JsonSerializerSettings(); + settings.Converters.Add(new TransientFaultExceptionConverter()); + settings.Converters.Add(new ExceptionConverter(includeStackTrace: true, includeData: false)); + + try + { + // Simulate a transient fault scenario with an inner exception + var innerException = new TimeoutException("Database connection timeout"); + + // Create an exception that wraps the transient fault context + var fault = new Exception("Failed to fetch user data after 3 attempts", innerException); + + // Serialize the exception - the converter will handle the serialization + var json = JsonConvert.SerializeObject(new { error = fault }, settings); + Console.WriteLine("Serialized fault with resilience context:"); + Console.WriteLine(json); + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + } + } +} +``` + +The converter preserves the complete evidence structure in JSON format, including: + +```json +{ + "message": "Failed to fetch user data after 3 attempts", + "evidence": { + "attempts": 3, + "recoveryWaitTime": "00:00:01", + "totalRecoveryWaitTime": "00:00:03", + "latency": "00:00:00.1500000", + "descriptor": { + "caller": "MyApplication.DataService", + "methodName": "FetchUserData", + "parameters": ["userId (Int32)"], + "arguments": [12345] + } + }, + "inner": { + "Type": "System.TimeoutException", + "Message": "Database connection timeout" + } +} +``` + +This converter is particularly useful for diagnostics and logging of resilience patterns, capturing the context and progression of transient fault handling. diff --git a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.DynamicContractResolver.md b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.DynamicContractResolver.md index a026b07..df3facd 100644 --- a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.DynamicContractResolver.md +++ b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.DynamicContractResolver.md @@ -1,29 +1,57 @@ ---- -uid: Codebelt.Extensions.Newtonsoft.Json.DynamicContractResolver -example: [*content] ---- - -## Examples - -`DynamicContractResolver` creates `IContractResolver` instances with per-property handler callbacks. Call `Create` where `T` is `CamelCasePropertyNamesContractResolver` or `DefaultContractResolver`. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.Newtonsoft.Json; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -var resolver = DynamicContractResolver.Create( - (pi, jp) => - { - if (pi.Name == "Id") - { - jp.PropertyName = "identifier"; - } - }); - -var settings = new JsonSerializerSettings { ContractResolver = resolver }; -var json = JsonConvert.SerializeObject(new { Id = 42, Name = "Alice" }, settings); -Console.WriteLine(json.Contains("identifier")); -``` +--- +uid: Codebelt.Extensions.Newtonsoft.Json.DynamicContractResolver +example: +- *content +--- + +Contract resolvers in JSON.NET determine how object properties are discovered, named, and serialized during JSON conversion, but creating custom resolvers typically requires subclassing `DefaultContractResolver` or `CamelCasePropertyNamesContractResolver` and overriding protected methods. This creates tight coupling to specific resolver implementations and makes it difficult to combine multiple customization strategies (property filtering, renaming, attribute handling) without complex class hierarchies. The `DynamicContractResolver` factory provides a simpler pattern: create a resolver instance using a factory method while passing handler delegates that customize `JsonProperty` metadata on a property-by-property basis. This enables composable, reusable property customization without subclassing. This example demonstrates creating a dynamic contract resolver with custom property handlers: + +```csharp +using System; +using System.Reflection; +using Codebelt.Extensions.Newtonsoft.Json; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Examples; + +public class DynamicResolverExample +{ + static void Main() + { + // Create a handler that marks all properties as required + void RequiredPropertyHandler(PropertyInfo info, JsonProperty property) + { + if (info != null && property != null) + { + property.Required = Required.Always; + } + } + + // Create a handler that skips properties with a certain name + void SkipPropertyHandler(PropertyInfo info, JsonProperty property) + { + if (info?.Name == "Secret") + { + property.Ignored = true; + } + } + + // Create a dynamic contract resolver with custom handlers + var resolver = DynamicContractResolver.Create( + RequiredPropertyHandler, + SkipPropertyHandler + ); + + var settings = new JsonSerializerSettings { ContractResolver = resolver }; + + var data = new { Name = "Alice", Secret = "hidden", Age = 30 }; + var json = JsonConvert.SerializeObject(data, settings); + + Console.WriteLine("Serialized with dynamic contract resolver:"); + Console.WriteLine(json); + } +} +``` + +The `DynamicContractResolver` factory method accepts the resolver type to create (e.g., `DefaultContractResolver`, `CamelCasePropertyNamesContractResolver`) and one or more handler delegates that customize each property's JSON representation. Handlers are invoked for every property discovered during contract resolution, enabling per-property customization without creating custom resolver subclasses. diff --git a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Formatters.NewtonsoftJsonFormatter.md b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Formatters.NewtonsoftJsonFormatter.md index 50b6aab..1ddbfd2 100644 --- a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Formatters.NewtonsoftJsonFormatter.md +++ b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Formatters.NewtonsoftJsonFormatter.md @@ -1,31 +1,153 @@ ---- -uid: Codebelt.Extensions.Newtonsoft.Json.Formatters.NewtonsoftJsonFormatter -example: [*content] ---- - -## Examples - -`NewtonsoftJsonFormatter` serializes and deserializes objects to and from JSON streams using Newtonsoft.Json. Use the static `SerializeObject` and `DeserializeObject` convenience methods for simple round-trips. - -```csharp -// Program.cs -using System; -using System.IO; -using Codebelt.Extensions.Newtonsoft.Json.Formatters; -using Newtonsoft.Json; - -var formatter = new NewtonsoftJsonFormatter(o => -{ - o.Settings.Formatting = Formatting.Indented; - o.Settings.NullValueHandling = NullValueHandling.Ignore; -}); - -var source = new { Message = "Hello" }; -using var stream = formatter.Serialize(source, source.GetType()); -var result = formatter.Deserialize(stream, source.GetType()); -Console.WriteLine(result?.GetType().GetProperty("Message")?.GetValue(result)); - -var json = NewtonsoftJsonFormatter.SerializeObject(new { Count = 42 }); -var deserialized = NewtonsoftJsonFormatter.DeserializeObject(json); -Console.WriteLine((int)deserialized.Count); -``` +--- +uid: Codebelt.Extensions.Newtonsoft.Json.Formatters.NewtonsoftJsonFormatter +example: +- *content +--- + +Applications need to convert domain objects to JSON for API responses, file persistence, and message queues, but each conversion scenario may require different settings: indentation for human debugging, null value handling for API contracts, custom converters for domain types. Writing serialization code inline is repetitive and error-prone; sharing a single `JsonSerializerSettings` instance across components is inflexible and difficult to test. The `NewtonsoftJsonFormatter` class provides a reusable, configurable JSON formatter that encapsulates Newtonsoft.Json with sensible defaults and supports both basic and advanced customization scenarios. This example demonstrates basic serialization and deserialization: + +```csharp +using System; +using System.IO; +using System.Text; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using Newtonsoft.Json; + +namespace Examples; + +public class Person +{ + public string Name { get; set; } + public int Age { get; set; } + public DateTime BirthDate { get; set; } +} + +public class BasicFormatterProgram +{ + public static void Main() + { + // Create formatter with default options + var formatter = new NewtonsoftJsonFormatter(); + + var person = new Person + { + Name = "Alice Smith", + Age = 30, + BirthDate = new DateTime(1994, 5, 15) + }; + + // Serialize to JSON stream + using (var jsonStream = formatter.Serialize(person, typeof(Person))) + { + var jsonString = Encoding.UTF8.GetString(((MemoryStream)jsonStream).ToArray()); + Console.WriteLine("Serialized JSON:"); + Console.WriteLine(jsonString); + Console.WriteLine(); + + // Deserialize back to object + jsonStream.Position = 0; + var deserializedPerson = formatter.Deserialize(jsonStream, typeof(Person)) as Person; + + Console.WriteLine("Deserialized Object:"); + Console.WriteLine($"Name: {deserializedPerson?.Name}"); + Console.WriteLine($"Age: {deserializedPerson?.Age}"); + } + } +} +``` + +### Custom Configuration + +Many applications need fine-grained control over formatting, null value handling, naming conventions, and custom converters for domain types (enums, flags, exceptions, etc.). Configuring each formatter instance from scratch is tedious and duplicates configuration logic across the codebase. The `NewtonsoftJsonFormatter` accepts a configuration delegate that customizes `JsonSerializerSettings` before formatters are created, enabling centralized configuration that can be unit-tested and easily reused. This example demonstrates configuring indented output, null handling, camelCase property names, and custom flag enum serialization: + +```csharp +using System; +using System.IO; +using System.Text; +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Examples; + +[Flags] +public enum UserRole { Admin = 1, User = 2, Guest = 4 } + +public class User +{ + public int Id { get; set; } + public string Username { get; set; } + public UserRole Roles { get; set; } +} + +public class CustomConfigProgram +{ + public static void Main() + { + // Create formatter with custom configuration + var formatter = new NewtonsoftJsonFormatter(options => + { + options.Settings.Formatting = Formatting.Indented; + options.Settings.NullValueHandling = NullValueHandling.Ignore; + options.Settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + + // Clear default converters and add custom ones + options.Settings.Converters.Clear(); + options.Settings.Converters.AddStringFlagsEnumConverter(); + options.Settings.Converters.AddStringEnumConverter(); + }); + + var user = new User + { + Id = 123, + Username = "alice", + Roles = UserRole.Admin | UserRole.User + }; + + using (var jsonStream = formatter.Serialize(user, typeof(User))) + { + var jsonString = Encoding.UTF8.GetString(((MemoryStream)jsonStream).ToArray()); + Console.WriteLine(jsonString); + } + } +} +``` + +### Synchronizing with JsonConvert + +The `JsonConvert.DefaultSettings` property allows setting global serialization behavior for ad-hoc JSON operations throughout an application, but it often conflicts with application-specific formatter settings and makes testing difficult. The `SynchronizeWithJsonConvert` option on `NewtonsoftJsonFormatterOptions` bridges this gap by registering the formatter's settings as the global default, ensuring consistent behavior between formatter instances and static `JsonConvert` calls without duplicating configuration. This example demonstrates synchronizing formatter settings with the global `JsonConvert.DefaultSettings`: + +```csharp +using System; +using System.IO; +using System.Text; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using Newtonsoft.Json; + +namespace Examples; + +public class SyncProgram +{ + public static void Main() + { + var formatter = new NewtonsoftJsonFormatter(options => + { + options.SynchronizeWithJsonConvert = true; + options.Settings.Formatting = Formatting.Indented; + options.Settings.NullValueHandling = NullValueHandling.Ignore; + }); + + // When SynchronizeWithJsonConvert is true, JsonConvert.SerializeObject will use the configured settings + var data = new { name = "test", value = (string)null }; + + using (var stream = formatter.Serialize(data, data.GetType())) + { + var jsonString = Encoding.UTF8.GetString(((MemoryStream)stream).ToArray()); + Console.WriteLine(jsonString); + } + } +} +``` + +The `NewtonsoftJsonFormatter` automatically refreshes converter dependencies based on sensitivity settings and can optionally synchronize with global `JsonConvert` settings. It inherits from `StreamFormatter` and provides both parameterless and configuration-based constructors for flexible initialization. diff --git a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Formatters.NewtonsoftJsonFormatterOptions.md b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Formatters.NewtonsoftJsonFormatterOptions.md index d39cd9d..cf3f892 100644 --- a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Formatters.NewtonsoftJsonFormatterOptions.md +++ b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Formatters.NewtonsoftJsonFormatterOptions.md @@ -1,28 +1,152 @@ ---- -uid: Codebelt.Extensions.Newtonsoft.Json.Formatters.NewtonsoftJsonFormatterOptions -example: [*content] ---- - -## Examples - -`NewtonsoftJsonFormatterOptions` configures the `NewtonsoftJsonFormatter` with `JsonSerializerSettings`, supported media types, sensitivity details for exception serialization, and whether to synchronize with `JsonConvert.DefaultSettings`. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.Newtonsoft.Json.Formatters; -using Newtonsoft.Json; - -var options = new NewtonsoftJsonFormatterOptions -{ - SynchronizeWithJsonConvert = true, - Settings = - { - Formatting = Formatting.Indented, - DateFormatString = "yyyy-MM-ddTHH:mm:ss.fffZ" - } -}; - -Console.WriteLine(options.Settings.Formatting == Formatting.Indented); -Console.WriteLine(options.SupportedMediaTypes.Count >= 3); -``` +--- +uid: Codebelt.Extensions.Newtonsoft.Json.Formatters.NewtonsoftJsonFormatterOptions +example: +- *content +--- + +Configuring JSON serialization requires setting many independent properties—formatting style, null handling, property naming conventions, custom converters—and these settings must be consistently applied across input formatters, output formatters, and exception handlers. Configuring each instance separately is error-prone and hides consistency issues until runtime. The `NewtonsoftJsonFormatterOptions` class centralizes JSON configuration in a single, immutable options object that can be validated, tested, and shared across all formatter instances in your application. This example demonstrates configuring formatter options with common serialization settings: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using Cuemon.Diagnostics; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace MyApplication +{ + public class OptionsConfigProgram + { + public static void Main() + { + var options = new NewtonsoftJsonFormatterOptions(); + + // Configure JSON serialization settings + options.Settings.Formatting = Formatting.Indented; + options.Settings.NullValueHandling = NullValueHandling.Ignore; + options.Settings.MissingMemberHandling = MissingMemberHandling.Ignore; + options.Settings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + + // Configure sensitivity for error responses + options.SensitivityDetails = FaultSensitivityDetails.StackTrace | FaultSensitivityDetails.Data; + + var formatter = new NewtonsoftJsonFormatter(options); + Console.WriteLine("Formatter options configured"); + } + } +} +``` + +### Customizing Media Types + +ASP.NET Core uses content negotiation to select input/output formatters based on request Accept headers and response Content-Type, but the default media type list (application/json, text/json, application/problem+json) may not match custom or vendor-specific media types your API supports. Without adding custom media types to the formatter, valid requests with "application/vnd.api+json" or other conventions are rejected even though your formatter could handle them. The `SupportedMediaTypes` property lets you extend the default list to match your API's contract, enabling content negotiation to succeed for additional media types. This example demonstrates configuring supported media types: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using System.Collections.Generic; +using System.Net.Http.Headers; + +namespace MyApplication +{ + public class MediaTypeProgram + { + public static void Main() + { + var options = new NewtonsoftJsonFormatterOptions(); + + // Default media types are application/json, text/json, and application/problem+json + // Customize if needed + options.SupportedMediaTypes = new List + { + new MediaTypeHeaderValue("application/json"), + new MediaTypeHeaderValue("application/vnd.api+json"), + new MediaTypeHeaderValue("text/json") + }; + + var formatter = new NewtonsoftJsonFormatter(options); + Console.WriteLine($"Configured {options.SupportedMediaTypes.Count} media types"); + + foreach (var mediaType in options.SupportedMediaTypes) + { + Console.WriteLine($" - {mediaType.MediaType}"); + } + } + } +} +``` + +### Exception Handling with Converters + +Exception responses in REST APIs must balance two competing needs: developers need detailed stack traces and error context for diagnostics, while external clients should see minimal details that don't expose implementation internals. A single formatter configuration can't satisfy both; you need per-audience sensitivity controls. The `SensitivityDetails` flag and custom exception converter registration enable environment-aware configuration where development APIs include stack traces and data while production APIs omit them, all from the same code base. This example demonstrates adding exception converters with sensitivity configuration: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Converters; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using Cuemon.Diagnostics; + +namespace MyApplication +{ + public class ExceptionHandlingProgram + { + public static void Main() + { + var options = new NewtonsoftJsonFormatterOptions(); + + // Configure for detailed exception information + options.SensitivityDetails = FaultSensitivityDetails.StackTrace + | FaultSensitivityDetails.Data; + + // Add custom converters + options.Settings.Converters.AddExceptionConverter( + includeStackTrace: true, + includeData: true + ); + + options.Settings.Converters.AddExceptionDescriptorConverterOf( + setup => setup.SensitivityDetails = options.SensitivityDetails + ); + + var formatter = new NewtonsoftJsonFormatter(options); + Console.WriteLine("Exception handling configured"); + } + } +} +``` + +### Synchronizing with JsonConvert + +Applications often use static `JsonConvert.SerializeObject` and `JsonConvert.DeserializeObject` calls throughout the codebase for quick JSON operations, but these calls don't automatically use the formatter's configuration, leading to inconsistent behavior between formatted responses and ad-hoc serialization. The `SynchronizeWithJsonConvert` flag addresses this by registering the formatter's settings as `JsonConvert.DefaultSettings`, ensuring all JSON operations in your application—whether via formatters or static calls—use the same configuration. This example demonstrates synchronizing with global JsonConvert settings: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using Newtonsoft.Json; + +namespace MyApplication +{ + public class JsonConvertSyncProgram + { + public static void Main() + { + var options = new NewtonsoftJsonFormatterOptions(); + + options.Settings.Formatting = Formatting.Indented; + options.Settings.NullValueHandling = NullValueHandling.Ignore; + + // Enable synchronization with JsonConvert.DefaultSettings + options.SynchronizeWithJsonConvert = true; + + var formatter = new NewtonsoftJsonFormatter(options); + + // Now JsonConvert.SerializeObject will use the configured settings + var testObject = new { test = "value", nullable = (string)null }; + var json = JsonConvert.SerializeObject(testObject); + Console.WriteLine(json); + } + } +} +``` + +The `NewtonsoftJsonFormatterOptions` class implements `IExceptionDescriptorOptions`, `IContentNegotiation`, and `IValidatableParameterObject`, providing comprehensive validation and configuration capabilities. The default configuration includes reasonable defaults for JSON formatting, null value handling, date parsing, and a set of commonly-used converters. diff --git a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JData.md b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JData.md index 4a17a99..407d6d1 100644 --- a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JData.md +++ b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JData.md @@ -1,26 +1,129 @@ ---- -uid: Codebelt.Extensions.Newtonsoft.Json.JData -example: [*content] ---- - -## Examples - -`JData` reads JSON from streams, strings, or `JsonReader` instances into `IEnumerable` sequences, enabling structured navigation of the parsed document without deserializing to a statically typed model. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.Newtonsoft.Json; -using System.Linq; - -var json = @"{ ""store"": { ""book"": [{ ""title"": ""Moby Dick"" }, { ""title"": ""Hamlet"" }] } }"; -var results = JData.ReadAll(json).ToList(); -var titles = results.Flatten() - .Where(r => r.PropertyName == "title") - .Select(r => r.Value); - -foreach (var title in titles) -{ - Console.WriteLine(title); -} -``` +--- +uid: Codebelt.Extensions.Newtonsoft.Json.JData +example: +- *content +--- + +Processing large JSON documents by fully deserializing them into memory requires buffering entire object graphs, consuming memory proportional to document size and preventing streaming architectures. Applications need a way to extract specific values or iterate over nested structures without materializing unused portions of the document. The `JData` class provides factory methods for RFC 7159-compliant streaming JSON parsing that yield values, paths, and types without requiring full deserialization, enabling efficient processing of large files and enabling downstream filtering or transformation pipelines. This example demonstrates how to parse a JSON string and extract structured results: + +```csharp +using Codebelt.Extensions.Newtonsoft.Json; +using System; +using System.Linq; + +namespace MyApplication +{ + public class JDataStringProgram + { + public static void Main() + { + var json = @"{ + ""name"": ""Alice"", + ""age"": 30, + ""email"": ""alice@example.com"" + }"; + + var results = JData.ReadAll(json).ToList(); + + foreach (var result in results) + { + Console.WriteLine($"Path: {result.Path}"); + Console.WriteLine($"Property: {result.PropertyName}"); + Console.WriteLine($"Value: {result.Value}"); + Console.WriteLine($"Type: {result.Type?.Name}"); + Console.WriteLine("---"); + } + } + } +} +``` + +### Reading from a Stream + +Serialized JSON often lives in files, network streams, or message queue payloads where reading it as a complete string in memory isn't practical. The `JData.ReadAll(Stream)` overload accepts a stream and optional configuration (character encoding, whether to leave the stream open), making it easy to parse JSON directly from `FileStream`, `NetworkStream`, or formatter output without buffering to string first. This approach scales to arbitrarily large documents while maintaining the same streaming parsing semantics as string-based calls. This example demonstrates how to read JSON from a stream with configuration: + +```csharp +using Codebelt.Extensions.Newtonsoft.Json; +using Codebelt.Extensions.Newtonsoft.Json.Formatters; +using System; +using System.IO; +using System.Linq; + +namespace MyApplication +{ + public class JDataStreamProgram + { + public static void Main() + { + var data = new + { + users = new[] + { + new { id = 1, name = "Alice" }, + new { id = 2, name = "Bob" } + } + }; + + var formatter = new NewtonsoftJsonFormatter(); + var jsonStream = formatter.Serialize(data, data.GetType()); + + // Read all values from stream with UTF-8 encoding + var results = JData.ReadAll(jsonStream, options => + { + options.LeaveOpen = true; + }).ToList(); + + Console.WriteLine($"Total tokens parsed: {results.Count}"); + + var userNames = results + .Where(r => r.PropertyName == "name") + .Select(r => r.Value) + .ToList(); + + foreach (var name in userNames) + { + Console.WriteLine($"User: {name}"); + } + } + } +} +``` + +### Reading from a JsonReader + +Newtonsoft.Json's `JsonReader` API gives fine-grained control over tokenization and supports custom token handling, custom error policies, and integration with Newtonsoft.Json's extension ecosystem. Some applications already use `JsonTextReader` directly or have custom reader implementations that need to be plugged into the streaming extraction pipeline. The `JData.ReadAll(JsonReader)` overload accepts any `JsonReader` instance, enabling composition with existing Newtonsoft.Json code and supporting advanced scenarios like nested readers or custom token processing. This example demonstrates direct usage with a `JsonReader`: + +```csharp +using Codebelt.Extensions.Newtonsoft.Json; +using Newtonsoft.Json; +using System; +using System.IO; +using System.Linq; + +namespace MyApplication +{ + public class JDataReaderProgram + { + public static void Main() + { + var json = @"[1, 2, 3, 4, 5]"; + + using (var sr = new StringReader(json)) + { + using (var reader = new JsonTextReader(sr)) + { + var results = JData.ReadAll(reader).ToList(); + + Console.WriteLine($"Array contains {results.Count} elements"); + foreach (var result in results) + { + Console.WriteLine($"Value: {result.Value}, Type: {result.Type?.Name}"); + } + } + } + } + } +} +``` + +The `JData` factory method returns an enumerable of `JDataResult` objects that provide hierarchical access to JSON structure through the `Path`, `Children`, and `Parent` properties. This approach enables efficient streaming parsing of large JSON documents without requiring full deserialization. diff --git a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JDataResult.md b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JDataResult.md index bd41727..dd6e469 100644 --- a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JDataResult.md +++ b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JDataResult.md @@ -1,23 +1,102 @@ ---- -uid: Codebelt.Extensions.Newtonsoft.Json.JDataResult -example: [*content] ---- - -## Examples - -`JDataResult` represents a node in the JSON tree produced by `JData.ReadAll`. Each node carries its path, property name, value, CLR type, children, and parent reference. - -```csharp -// Program.cs -using System; -using System.Linq; -using Codebelt.Extensions.Newtonsoft.Json; - -var json = @"{ ""name"": ""Alice"", ""age"": 30 }"; -JDataResult[] results = JData.ReadAll(json).ToArray(); - -foreach (JDataResult r in results) -{ - Console.WriteLine($"{r.Path}: {r.PropertyName} = {r.Value} ({r.Type.Name})"); -} -``` +--- +uid: Codebelt.Extensions.Newtonsoft.Json.JDataResult +example: +- *content +--- + +The `JDataResult` class represents a single parsed result from a JSON reading operation, providing access to the token's path, value, type, and hierarchical structure. The following example demonstrates how to work with `JDataResult` objects: + +```csharp +using Codebelt.Extensions.Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace MyApplication +{ + public class JDataResultProgram + { + public static void Main() + { + var json = @"{ + ""user"": { + ""id"": 1, + ""profile"": { + ""firstName"": ""John"", + ""lastName"": ""Doe"", + ""tags"": [""admin"", ""user"", ""developer""] + } + } + }"; + + var results = JData.ReadAll(json).ToList(); + + // Find results with specific properties + var firstNameResult = results.FirstOrDefault(r => r.PropertyName == "firstName"); + if (firstNameResult != null) + { + Console.WriteLine($"Property: {firstNameResult.PropertyName}"); + Console.WriteLine($"Value: {firstNameResult.Value}"); + Console.WriteLine($"Path: {firstNameResult.Path}"); + Console.WriteLine($"Type: {firstNameResult.Type?.Name}"); + } + + // Find results with children (complex objects/arrays) + var complexResults = results.Where(r => r.Children.Count > 0).ToList(); + Console.WriteLine($"Complex structures found: {complexResults.Count}"); + + // Navigate hierarchy + var profileResults = results.Where(r => r.PropertyName == "profile").ToList(); + foreach (var profileResult in profileResults) + { + Console.WriteLine($"Profile has {profileResult.Children.Count} children"); + foreach (var child in profileResult.Children) + { + Console.WriteLine($" - {child.PropertyName}: {child.Value}"); + } + } + + // Find results with parent references + var rootResults = results.Where(r => r.Parent == null).ToList(); + Console.WriteLine($"Root level results: {rootResults.Count}"); + + // Traverse and print structure + PrintHierarchy(results.Where(r => r.Parent == null).ToList(), 0); + } + + private static void PrintHierarchy(List results, int indent) + { + foreach (var result in results) + { + var indentation = new string(' ', indent * 2); + var displayValue = result.Value != null ? result.Value.ToString() : "[object]"; + + if (result.PropertyName != null) + { + Console.WriteLine($"{indentation}{result.PropertyName}: {displayValue}"); + } + else + { + Console.WriteLine($"{indentation}{displayValue}"); + } + + if (result.Children.Count > 0) + { + PrintHierarchy(result.Children.ToList(), indent + 1); + } + } + } + } +} +``` + +The `JDataResult` class provides the following properties: + +- **Path**: The JSON path to the token (e.g., "user.profile.firstName") +- **PropertyName**: The name of the property if this result represents a property +- **Value**: The parsed value of the token +- **Type**: The CLR type of the value +- **Children**: A collection of child results for complex types (objects/arrays) +- **Parent**: A reference to the parent result in the hierarchy + +The `ToString()` method provides a formatted representation showing the path and child count, making it useful for diagnostics and logging. diff --git a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JDataResultExtensions.md b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JDataResultExtensions.md index 9f3f286..d7e2b18 100644 --- a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JDataResultExtensions.md +++ b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JDataResultExtensions.md @@ -1,34 +1,188 @@ ---- -uid: Codebelt.Extensions.Newtonsoft.Json.JDataResultExtensions -example: -- *content ---- - -`JDataResultExtensions` helps you flatten a `JDataResult` tree and then extract values by the JSON paths produced by that flattened sequence. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.Newtonsoft.Json; -using System.Linq; - -var json = @"{ ""book"": { ""title"": ""Moby Dick"", ""price"": 12.99 } }"; -var results = JData.ReadAll(json).Flatten().ToList(); -results.ExtractObjectValues("book.title, book.price", dict => -{ - foreach (var kv in dict) - { - Console.WriteLine($"{kv.Key}: {kv.Value.Value}"); - } -}); - -var json2 = @"{ ""items"": [{ ""id"": 1 }, { ""id"": 2 }] }"; -var results2 = JData.ReadAll(json2).ToList(); -results2.ExtractArrayValues("items", dict => -{ - foreach (var kv in dict) - { - Console.WriteLine($"{kv.Key}: {kv.Value.Count()} items"); - } -}); -``` +--- +uid: Codebelt.Extensions.Newtonsoft.Json.JDataResultExtensions +example: +- *content +--- +The following example shows how to parse JSON and flatten its hierarchical structure. When working with complex nested JSON documents—API responses, configuration files, or database exports—developers need to search for specific properties, validate values at any depth, or transform data without writing recursive navigation code. The `JDataResultExtensions` class provides extension methods to flatten nested structures, extract specific properties or arrays, and process results using standard LINQ queries. This approach transforms JSON navigation from imperative recursion into declarative LINQ queries that are easier to read, maintain, and test. This example demonstrates parsing multi-level JSON with users and tags, then using extension methods to flatten, extract objects by property names, and group array structures: + +```csharp +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Codebelt.Extensions.Newtonsoft.Json; + +namespace Examples; + +class JDataResultExtensionsExample +{ + static void Main() + { + var json = """ + { + "users": [ + { "id": 1, "name": "Alice", "age": 30 }, + { "id": 2, "name": "Bob", "age": 25 } + ], + "tags": ["admin", "user"] + } + """; + + var results = JData.ReadAll(json); + var flat = results.Flatten().ToList(); + + // Demonstrate flattening + foreach (var item in flat) + { + if (!string.IsNullOrEmpty(item.PropertyName)) + { + Console.WriteLine($"{item.Path}: {item.Value}"); + } + } + + // Demonstrate ExtractObjectValues to extract specific properties from objects + flat.ExtractObjectValues("id,name,age", extracted => + { + var id = extracted["id"].Value; + var name = extracted["name"].Value; + var age = extracted["age"].Value; + Console.WriteLine($"Extracted: id={id}, name={name}, age={age}"); + }); + + // Demonstrate ExtractArrayValues to extract and process arrays separately + flat.ExtractArrayValues("users,tags", extracted => + { + var userCount = extracted["users"].Count(); + var tagCount = extracted["tags"].Count(); + Console.WriteLine($"Grouped arrays: found {userCount} users and {tagCount} tags"); + }); + } +} +``` + +--- +uid: Codebelt.Extensions.Newtonsoft.Json.JDataResultExtensions.Flatten +example: +- *content +--- + +Nested JSON structures require traversal and filtering to extract specific properties or understand hierarchical organization, but working with raw `JDataResult` collections that preserve parent-child relationships requires manual recursion or LINQ filtering. The `Flatten` extension method transforms hierarchical `JDataResult` sequences into a single flat enumerable while preserving path information, enabling straightforward LINQ-to-Objects queries for finding properties by name, filtering by value type, or extracting deep values without custom traversal logic. This is essential for applications that need to search, transform, or validate arbitrarily nested JSON documents without detailed knowledge of structure. This example demonstrates parsing multi-level JSON with objects and arrays, then flattening the result and using LINQ to find properties and filter values: + +```csharp +using System; +using System.Collections.Generic; +using System.Linq; +using Codebelt.Extensions.Newtonsoft.Json; + +namespace Examples; + +class FlattenExample +{ + static void Main() + { + var json = """ + { + "level1": { + "level2": { + "value": "deep" + } + } + } + """; + + var results = JData.ReadAll(json); + var flatList = results.Flatten().ToList(); + + Console.WriteLine($"Flattened {flatList.Count} items from nested structure"); + foreach (var item in flatList.Where(r => !string.IsNullOrEmpty(r.Value?.ToString()))) + { + Console.WriteLine($" {item.Path}: {item.Value}"); + } + } +} +``` + +--- +uid: Codebelt.Extensions.Newtonsoft.Json.JDataResultExtensions.ExtractObjectValues +example: +- *content +--- + +Working with JSON arrays of objects often requires extracting specific properties from each object to build dictionaries or maps for processing. Manually iterating arrays, finding matching properties, and organizing them into lookup structures is error-prone and verbose, especially when dealing with variable numbers of objects and deeply nested property structures. The `ExtractObjectValues` extension method simplifies this pattern by accepting a comma-delimited list of property paths and invoking a callback for each object with a dictionary of extracted properties, enabling clean batch processing of JSON arrays without manual iteration or filtering. This is particularly useful for ETL pipelines, data transformation scripts, and APIs that need to extract columns from semi-structured JSON where the source structure may vary. This example demonstrates parsing a JSON array of person objects, flattening to access all properties, then using the extension method to extract name and age fields and transform each person into application objects: + +```csharp +using System; +using System.Collections.Generic; +using System.Linq; +using Codebelt.Extensions.Newtonsoft.Json; + +namespace Examples; + +class ExtractObjectValuesExample +{ + static void Main() + { + var json = """ + [ + { "name": "Alice", "age": 30 }, + { "name": "Bob", "age": 25 } + ] + """; + + var results = JData.ReadAll(json); + var flat = results.Flatten().ToList(); + + // Call ExtractObjectValues to group and process properties + flat.ExtractObjectValues("name,age", extracted => + { + var name = extracted["name"].Value; + var age = extracted["age"].Value; + Console.WriteLine($"Extracted person: {name}, age {age}"); + }); + } +} +``` + +--- +uid: Codebelt.Extensions.Newtonsoft.Json.JDataResultExtensions.ExtractArrayValues +example: +- *content +--- + +JSON documents often contain multiple arrays at different locations (users, tags, items, etc.) that need to be separately extracted and analyzed, but identifying which results correspond to which arrays and grouping them requires complex filtering logic that obscures the intent. The `ExtractArrayValues` extension method accepts a comma-delimited list of array paths and invokes a callback with grouped results for each array, making it straightforward to process multiple arrays in a single pass without custom filtering and grouping code. This pattern is essential for data validation, aggregation, and transformation scenarios where you need to count array lengths, validate membership, or apply transformations to array elements independently. This example demonstrates parsing mixed JSON with both user and tag arrays, flattening the result, then using the extension method to extract and process users and tags separately, enabling independent aggregation and reporting: + +```csharp +using System; +using System.Collections.Generic; +using System.Linq; +using Codebelt.Extensions.Newtonsoft.Json; + +namespace Examples; + +class ExtractArrayValuesExample +{ + static void Main() + { + var json = """ + { + "users": [ + { "id": 1, "name": "Alice" }, + { "id": 2, "name": "Bob" } + ], + "tags": ["admin", "user"] + } + """; + + var results = JData.ReadAll(json); + var flat = results.Flatten().ToList(); + + // Call ExtractArrayValues to group and process array elements + flat.ExtractArrayValues("users,tags", extracted => + { + var userCount = extracted["users"].Count(); + var tagCount = extracted["tags"].Count(); + Console.WriteLine($"Grouped arrays: found {userCount} users and {tagCount} tags"); + }); + } +} +``` diff --git a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JsonConverterFactory.md b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JsonConverterFactory.md index d957a0f..6fbbefa 100644 --- a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JsonConverterFactory.md +++ b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JsonConverterFactory.md @@ -1,30 +1,71 @@ ---- -uid: Codebelt.Extensions.Newtonsoft.Json.JsonConverterFactory -example: [*content] ---- - -## Examples - -`JsonConverterFactory` creates `JsonConverter` instances from delegates without defining a custom converter class. Call `Create` to make a converter for a specific type with writer and optional reader lambdas. Use `Create(Func, ...)` for a custom `CanConvert` predicate. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.Newtonsoft.Json; -using Newtonsoft.Json; - -var settings = new JsonSerializerSettings(); -settings.Converters.Add(JsonConverterFactory.Create( - (writer, value, serializer) => writer.WriteValue(value.ToString("O")))); - -var json = JsonConvert.SerializeObject(DateTime.UtcNow, settings); -Console.WriteLine(json.StartsWith("\"") && json.EndsWith("\"")); - -var settings2 = new JsonSerializerSettings(); -settings2.Converters.Add(JsonConverterFactory.Create( - t => t.IsEnum, - (writer, value, serializer) => writer.WriteValue(value.ToString()?.ToLowerInvariant()))); - -var json2 = JsonConvert.SerializeObject(StringComparison.OrdinalIgnoreCase, settings2); -Console.WriteLine(json2 == "\"ordinalignorecase\""); -``` +--- +uid: Codebelt.Extensions.Newtonsoft.Json.JsonConverterFactory +example: +- *content +--- + +Creating custom JSON converters typically requires subclassing `JsonConverter` and overriding `WriteJson` and `ReadJson` methods, which introduces boilerplate code and tight coupling to the converter base class. For simple conversions that don't require complex state management, full subclassing is overkill. The `JsonConverterFactory` provides a lightweight factory pattern that creates converters from simple delegate functions—one for writing objects to JSON and one for reading from JSON. This enables ad-hoc converter creation without subclassing, supporting quick customization for domain-specific types, legacy formats, and compatibility scenarios. This example demonstrates creating a custom converter for a coordinate type: + +```csharp +using System; +using System.Globalization; +using Codebelt.Extensions.Newtonsoft.Json; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Examples; + +public class Coordinate +{ + public double Latitude { get; set; } + public double Longitude { get; set; } + + public Coordinate() { } + public Coordinate(double latitude, double longitude) + { + Latitude = latitude; + Longitude = longitude; + } +} + +public class ConverterFactoryExample +{ + static void Main() + { + // Create a converter that serializes Coordinate as "lat,lng" + var converter = JsonConverterFactory.Create( + writer: (jw, coord, serializer) => + { + jw.WriteValue($"{coord.Latitude:F4},{coord.Longitude:F4}"); + }, + reader: (jr, type, currentValue, serializer) => + { + if (jr.TokenType == JsonToken.String && jr.Value is string coordStr) + { + var parts = coordStr.Split(','); + if (parts.Length == 2 && + double.TryParse(parts[0], NumberStyles.Float, CultureInfo.InvariantCulture, out var lat) && + double.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var lng)) + { + return new Coordinate(lat, lng); + } + } + return currentValue; + } + ); + + var settings = new JsonSerializerSettings(); + settings.Converters.Add(converter); + + var location = new Coordinate(51.5074, -0.1278); // London + var json = JsonConvert.SerializeObject(location, settings); + + Console.WriteLine($"Serialized coordinate: {json}"); + + var deserialized = JsonConvert.DeserializeObject(json, settings); + Console.WriteLine($"Deserialized: Latitude={deserialized.Latitude}, Longitude={deserialized.Longitude}"); + } +} +``` + +The `JsonConverterFactory` accepts write and read delegates that implement custom serialization logic for a specific type. The factory creates a `JsonConverter` instance that can be registered in `JsonSerializerSettings.Converters` and will intercept serialization and deserialization of that type using the provided delegates. diff --git a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JsonSerializerSettingsExtensions.md b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JsonSerializerSettingsExtensions.md index c7d94f1..c6f250d 100644 --- a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JsonSerializerSettingsExtensions.md +++ b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JsonSerializerSettingsExtensions.md @@ -1,24 +1,64 @@ ---- -uid: Codebelt.Extensions.Newtonsoft.Json.JsonSerializerSettingsExtensions -example: [*content] ---- - -## Examples - -`JsonSerializerSettingsExtensions.ApplyToDefaultSettings` sets `JsonConvert.DefaultSettings` to a factory that returns the configured `JsonSerializerSettings`, making all subsequent `JsonConvert` calls use the applied configuration. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.Newtonsoft.Json; -using Codebelt.Extensions.Newtonsoft.Json.Formatters; -using Newtonsoft.Json; - -var options = new NewtonsoftJsonFormatterOptions(); -options.Settings.Formatting = Formatting.None; -options.Settings.NullValueHandling = NullValueHandling.Ignore; -options.Settings.ApplyToDefaultSettings(); - -var json = JsonConvert.SerializeObject(new { Name = "Alice", Age = (int?)null }); -Console.WriteLine(json.Contains("null") == false); -``` +--- +uid: Codebelt.Extensions.Newtonsoft.Json.JsonSerializerSettingsExtensions +example: +- *content +--- +The following example applies custom serializer settings to the default JSON serialization options. + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json; +using Newtonsoft.Json; + +namespace Examples; + +class JsonSerializerSettingsExtensionsExample +{ + static void Main() + { + var settings = new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore, + Formatting = Formatting.Indented + }; + + settings.ApplyToDefaultSettings(); + Console.WriteLine("Custom settings applied to defaults"); + + var obj = new { name = "Test", value = (string)null }; + var json = JsonConvert.SerializeObject(obj); + Console.WriteLine(json); + } +} +``` + +--- +uid: Codebelt.Extensions.Newtonsoft.Json.JsonSerializerSettingsExtensions.ApplyToDefaultSettings +example: +- *content +--- + +Applications that use `JsonConvert.SerializeObject` and `JsonConvert.DeserializeObject` static methods need a way to globally configure the default serialization behavior without directly modifying the static `JsonConvert.DefaultSettings` property. The `ApplyToDefaultSettings` extension method provides a clean API to register custom converters, null value handling, formatting, and other settings into the global default settings, ensuring all subsequent JSON.NET static method calls inherit the application's serialization preferences. This is essential for applications that mix static convenience methods with formatter instances and need consistent behavior across both paths. This example demonstrates applying custom date handling and null value ignoring to the global defaults: + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json; +using Newtonsoft.Json; + +namespace Examples; + +class Program +{ + static void Main() + { + var settings = new JsonSerializerSettings + { + DateFormatString = "yyyy-MM-dd", + DefaultValueHandling = DefaultValueHandling.Ignore + }; + + settings.ApplyToDefaultSettings(); + Console.WriteLine("Custom settings are now default for JSON operations"); + } +} +``` diff --git a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JsonWriterExtensions.md b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JsonWriterExtensions.md index 8015b01..9aada94 100644 --- a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JsonWriterExtensions.md +++ b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.JsonWriterExtensions.md @@ -1,34 +1,115 @@ ---- -uid: Codebelt.Extensions.Newtonsoft.Json.JsonWriterExtensions -example: [*content] ---- - -## Examples - -`JsonWriterExtensions` provides `WritePropertyName` and `WriteObject` for `JsonWriter`. `WritePropertyName` resolves the naming strategy from the serializer's contract resolver; `WriteObject` delegates to `serializer.Serialize(writer, value)`. - -```csharp -// Program.cs -using System; -using System.IO; -using System.Text; -using Codebelt.Extensions.Newtonsoft.Json; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; - -var sb = new StringBuilder(); -using var sw = new StringWriter(sb); -using var writer = new JsonTextWriter(sw); -var serializer = JsonSerializer.Create(new JsonSerializerSettings -{ - ContractResolver = new CamelCasePropertyNamesContractResolver() -}); - -writer.WriteStartObject(); -writer.WritePropertyName("MyProperty", serializer); -writer.WriteObject(new { x = 1, y = 2 }, serializer); -writer.WriteEndObject(); - -Console.WriteLine(sb.ToString().Contains("myProperty")); -Console.WriteLine(sb.ToString().Contains("\"x\":1")); -``` +--- +uid: Codebelt.Extensions.Newtonsoft.Json.JsonWriterExtensions +example: +- *content +--- +The following example demonstrates writing JSON objects and property names using extension methods. + +```csharp +using System; +using System.IO; +using Codebelt.Extensions.Newtonsoft.Json; +using Newtonsoft.Json; + +namespace Examples; + +class JsonWriterExtensionsExample +{ + static void Main() + { + using var sw = new StringWriter(); + using var jw = new JsonTextWriter(sw); + var serializer = JsonSerializer.Create(); + + jw.WriteStartObject(); + jw.WritePropertyName("user"); + jw.WriteStartObject(); + jw.WritePropertyName("name"); + jw.WriteValue("Alice"); + jw.WriteEndObject(); + + jw.WritePropertyName("metadata"); + var metadataObject = new { version = "1.0", timestamp = DateTime.UtcNow }; + jw.WriteObject(metadataObject, serializer); + + jw.WriteEndObject(); + + Console.WriteLine(sw.ToString()); + } +} +``` + +--- +uid: Codebelt.Extensions.Newtonsoft.Json.JsonWriterExtensions.WriteObject +example: +- *content +--- + +Building JSON documents programmatically with `JsonWriter` often requires creating nested objects and writing properties with values that are themselves objects or complex types. The base `JsonWriter` API requires manual calls to `WriteStartObject`, `WritePropertyName`, and `WriteEndObject` for each nested level, leading to verbose, error-prone code with a high risk of mismatched braces. The `WriteObject` extension method provides a convenient shorthand that accepts an anonymous object, ExpandoObject, or other object, automatically serializes it, and writes it to the current JSON writer in a single statement. This dramatically simplifies nested object creation and reduces bracket-matching errors in large JSON construction workflows. This example demonstrates writing a nested object as a single property value: + +```csharp +using System; +using System.IO; +using Codebelt.Extensions.Newtonsoft.Json; +using Newtonsoft.Json; + +namespace Examples; + +class WriteObjectExample +{ + static void Main() + { + using var sw = new StringWriter(); + using var jw = new JsonTextWriter(sw); + + // Create a serializer for the WriteObject call + var serializer = JsonSerializer.Create(); + + jw.WriteStartObject(); + jw.WritePropertyName("status"); + jw.WriteValue("active"); + jw.WritePropertyName("metadata"); + + // Call WriteObject extension to write a nested object + var metadataObject = new { version = "1.0", timestamp = DateTime.UtcNow }; + jw.WriteObject(metadataObject, serializer); + + jw.WriteEndObject(); + + Console.WriteLine($"Written nested object: {sw.ToString()}"); + } +} +``` + +--- +uid: Codebelt.Extensions.Newtonsoft.Json.JsonWriterExtensions.WritePropertyName +example: +- *content +--- + +Writing JSON property names with `JsonWriter.WritePropertyName` requires manual escaping and validation to handle special characters, quotes, and non-ASCII characters according to JSON specification. Applications building dynamic JSON documents with property names derived from user input, database columns, or external sources need a reliable way to write safely-escaped property names without worrying about RFC 7159 compliance. The `WritePropertyName` extension method wraps the base writer method with validation and escaping, ensuring property names are always written in valid JSON format regardless of their source. This is particularly important for applications that generate JSON with programmatically-determined property names. This example demonstrates writing property names with special characters: + +```csharp +using System; +using System.IO; +using Codebelt.Extensions.Newtonsoft.Json; +using Newtonsoft.Json; + +namespace Examples; + +class WritePropertyNameExample +{ + static void Main() + { + using var sw = new StringWriter(); + using var jw = new JsonTextWriter(sw); + + jw.WriteStartObject(); + jw.WritePropertyName("special:field"); + jw.WriteValue("value"); + jw.WriteEndObject(); + + Console.WriteLine($"Property written: {sw.ToString()}"); + } +} +``` diff --git a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Serialization.ContractResolverExtensions.md b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Serialization.ContractResolverExtensions.md index 480ae6e..a886ec9 100644 --- a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Serialization.ContractResolverExtensions.md +++ b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.Serialization.ContractResolverExtensions.md @@ -1,23 +1,48 @@ ---- -uid: Codebelt.Extensions.Newtonsoft.Json.Serialization.ContractResolverExtensions -example: [*content] ---- - -## Examples - -`ContractResolverExtensions.ResolveNamingStrategyOrDefault` extracts the `NamingStrategy` from any `IContractResolver`, falling back to `CamelCaseNamingStrategy` for null or unresolvable instances. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.Newtonsoft.Json.Serialization; -using Newtonsoft.Json.Serialization; - -var resolver = new CamelCasePropertyNamesContractResolver(); -var strategy = resolver.ResolveNamingStrategyOrDefault(); -Console.WriteLine(strategy is CamelCaseNamingStrategy); - -var resolver2 = new DefaultContractResolver(); -var strategy2 = resolver2.ResolveNamingStrategyOrDefault(); -Console.WriteLine(strategy2 is DefaultNamingStrategy); -``` +--- +uid: Codebelt.Extensions.Newtonsoft.Json.Serialization.ContractResolverExtensions +example: +- *content +--- +The following example resolves or provides a default naming strategy from a contract resolver. + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json.Serialization; +using Newtonsoft.Json.Serialization; + +namespace Examples; + +class ContractResolverExtensionsExample +{ + static void Main() + { + var resolver = new DefaultContractResolver(); + var namingStrategy = resolver.ResolveNamingStrategyOrDefault(); + + Console.WriteLine($"Naming strategy type: {namingStrategy.GetType().Name}"); + var converted = namingStrategy.GetPropertyName("UserName", false); + Console.WriteLine($"UserName converted to: {converted}"); + } +} +``` + +--- +uid: Codebelt.Extensions.Newtonsoft.Json.Serialization.ContractResolverExtensions.ResolveNamingStrategyOrDefault +example: +- *content +--- + +Contract resolvers in JSON.NET support optional `NamingStrategy` instances that transform property names during serialization (camelCase, snake_case, PascalCase, etc.), but retrieving the naming strategy requires inspecting resolver properties that may be null or missing depending on the resolver type. Applications that need to apply the same naming transformation used by a contract resolver to property names in different contexts—logs, error messages, API documentation generation—need a reliable way to extract the naming strategy without type-specific knowledge. The `ResolveNamingStrategyOrDefault` extension method returns the resolver's naming strategy if present, or a default pass-through strategy if none is configured, ensuring callers always receive a valid strategy for property name transformation. This example demonstrates resolving the naming strategy and applying it to a property name: + +```csharp +// Program.cs +using System; +using Codebelt.Extensions.Newtonsoft.Json.Serialization; +using Newtonsoft.Json.Serialization; + +var resolver = new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() }; +var strategy = resolver.ResolveNamingStrategyOrDefault(); +var result = strategy.GetPropertyName("FirstName", false); + +Console.WriteLine($"FirstName becomes: {result}"); +``` diff --git a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.ValidatorExtensions.md b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.ValidatorExtensions.md index 0fe40bf..cb3296b 100644 --- a/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.ValidatorExtensions.md +++ b/.docfx/api/types/Codebelt.Extensions.Newtonsoft.Json.ValidatorExtensions.md @@ -1,28 +1,61 @@ ---- -uid: Codebelt.Extensions.Newtonsoft.Json.ValidatorExtensions -example: [*content] ---- - -## Examples - -`ValidatorExtensions` extends `Validator` with `InvalidJsonDocument` to guard method arguments against invalid JSON strings and `JsonReader` instances. - -```csharp -// Program.cs -using System; -using Codebelt.Extensions.Newtonsoft.Json; -using Cuemon; - -var validJson = @"{ ""id"": ""abc-123"" }"; -Validator.ThrowIf.InvalidJsonDocument(validJson, paramName: "validJson"); - -try -{ - var invalidJson = @"{ broken"; - Validator.ThrowIf.InvalidJsonDocument(invalidJson, paramName: "invalidJson"); -} -catch (ArgumentException ex) -{ - Console.WriteLine(ex.Message.StartsWith("Value must be a JSON representation")); -} -``` +--- +uid: Codebelt.Extensions.Newtonsoft.Json.ValidatorExtensions +example: +- *content +--- +The following example validates JSON document format before parsing. + +```csharp +using System; +using Codebelt.Extensions.Newtonsoft.Json; +using Cuemon; + +namespace Examples; + +class ValidatorExtensionsExample +{ + static void Main() + { + var validJson = """{ "id": "123", "name": "Test" }"""; + var invalidJson = """{ "id" "123" }"""; + + try + { + Validator.ThrowIf.InvalidJsonDocument(validJson); + Console.WriteLine("Valid JSON passed validation"); + } + catch (ArgumentException ex) + { + Console.WriteLine($"Validation failed: {ex.Message}"); + } + + try + { + Validator.ThrowIf.InvalidJsonDocument(invalidJson); + } + catch (ArgumentException ex) + { + Console.WriteLine($"Invalid JSON caught: {ex.Message}"); + } + } +} +``` + +--- +uid: Codebelt.Extensions.Newtonsoft.Json.ValidatorExtensions.InvalidJsonDocument +example: +- *content +--- + +JSON validation is a critical first step in any data pipeline that consumes untrusted JSON, preventing downstream parsing errors, deserialization exceptions, and cryptic error messages that frustrate developers and obscure the true source of invalid data. The `InvalidJsonDocument` validator method checks that a JSON string complies with RFC 8259 specification, throwing `ArgumentException` with a descriptive message if the document is malformed, enabling early failure and clear diagnostics. Applications can integrate this validator into request pipelines, configuration loaders, and data transformation steps to guarantee valid JSON before proceeding with parsing or deserialization. This example demonstrates validating a JSON string before further processing: + +```csharp +// Program.cs +using System; +using Codebelt.Extensions.Newtonsoft.Json; +using Cuemon; + +var json = """{ "status": "ok", "code": 200 }"""; +Validator.ThrowIf.InvalidJsonDocument(json, nameof(json)); +Console.WriteLine("JSON is valid"); +``` diff --git a/.nuget/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json/PackageReleaseNotes.txt index 9c033b1..50bc103 100644 --- a/.nuget/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version: 10.1.7 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + Version: 10.1.6 Availability: .NET 10 and .NET 9 diff --git a/.nuget/Codebelt.Extensions.AspNetCore.Newtonsoft.Json/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.AspNetCore.Newtonsoft.Json/PackageReleaseNotes.txt index b939f1a..fda3e05 100644 --- a/.nuget/Codebelt.Extensions.AspNetCore.Newtonsoft.Json/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.AspNetCore.Newtonsoft.Json/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version: 10.1.7 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + Version: 10.1.6 Availability: .NET 10 and .NET 9 diff --git a/.nuget/Codebelt.Extensions.Newtonsoft.Json.App/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.Newtonsoft.Json.App/PackageReleaseNotes.txt index 81236dd..9a1c996 100644 --- a/.nuget/Codebelt.Extensions.Newtonsoft.Json.App/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.Newtonsoft.Json.App/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version: 10.1.7 +Availability: .NET 10 and .NET 9 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + Version: 10.1.6 Availability: .NET 10 and .NET 9 diff --git a/.nuget/Codebelt.Extensions.Newtonsoft.Json/PackageReleaseNotes.txt b/.nuget/Codebelt.Extensions.Newtonsoft.Json/PackageReleaseNotes.txt index b366429..c390b07 100644 --- a/.nuget/Codebelt.Extensions.Newtonsoft.Json/PackageReleaseNotes.txt +++ b/.nuget/Codebelt.Extensions.Newtonsoft.Json/PackageReleaseNotes.txt @@ -1,3 +1,9 @@ +Version: 10.1.7 +Availability: .NET 10, .NET 9 and .NET Standard 2.0 + +# ALM +- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs) + Version: 10.1.6 Availability: .NET 10, .NET 9 and .NET Standard 2.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 99782c6..d0f0848 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,28 @@ For more details, please refer to `PackageReleaseNotes.txt` on a per assembly ba > [!NOTE] > Changelog entries prior to version 8.4.0 was migrated from previous versions of Cuemon.Extensions.Newtonsoft.Json, Cuemon.Extensions.AspNetCore.Newtonsoft.Json and Cuemon.Extensions.AspNetCore.Mvc.Formatters.Newtonsoft.Json. +## [10.1.7] - 2026-08-16 + +This is a patch release focused on dependency service updates, test infrastructure consolidation, and enhanced API documentation. + +### Changed + +- Package dependencies upgraded: + - Codebelt.Extensions.Xunit (11.1.2 → 11.2.1), + - Codebelt.Extensions.Xunit.App (11.1.2 → 11.2.1), + - Cuemon.AspNetCore.Mvc (10.5.5 → 10.7.0), + - Cuemon.Core (10.5.5 → 10.7.0), + - Cuemon.Extensions.AspNetCore (10.5.5 → 10.7.0), + - Cuemon.Extensions.AspNetCore.Authentication (10.5.5 → 10.7.0), + - Cuemon.Extensions.AspNetCore.Mvc (10.5.5 → 10.7.0), + - Cuemon.Extensions.Core (10.5.5 → 10.7.0), + - Cuemon.Extensions.IO (10.5.5 → 10.7.0), + - Cuemon.IO (10.5.5 → 10.7.0), + - Microsoft.NET.Test.Sdk (18.8.1 → 18.9.0), + - Microsoft.AspNetCore.Mvc.NewtonsoftJson (9.0.17 → 9.0.19 for net9, 10.0.9 → 10.0.11 for net10), +- Test environment configuration simplified by consolidating separate Docker test runners for net9 and net10 into unified codebeltnet/ubuntu-testrunner:8-9-10-11 image, ensuring consistent test execution across all supported TFMs, +- DocFX type documentation enhanced with comprehensive usage examples, demonstrating core capabilities including streaming JSON parsing, formatter integration, converter usage, and configuration patterns for both ASP.NET Core and standalone applications. + ## [10.1.6] - 2026-07-23 This is a patch release focused on dependency service updates, tighter source-project build analysis, and maintainability cleanups around ExceptionConverter and modern C# patterns. @@ -310,6 +332,7 @@ This major release is first and foremost focused on ironing out any wrinkles tha - JsonReaderResultExtensions class from the Codebelt.Extensions.Newtonsoft.Json namespace - JsonReaderParser class from the Codebelt.Extensions.Newtonsoft.Json namespace +[10.1.7]: https://github.com/codebeltnet/newtonsoft-json/compare/v10.1.6...v10.1.7 [10.1.6]: https://github.com/codebeltnet/newtonsoft-json/compare/v10.1.5...v10.1.6 [10.1.5]: https://github.com/codebeltnet/newtonsoft-json/compare/v10.1.4...v10.1.5 [10.1.4]: https://github.com/codebeltnet/newtonsoft-json/compare/v10.1.3...v10.1.4 diff --git a/Directory.Packages.props b/Directory.Packages.props index 65599cb..f4a0021 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,17 +3,17 @@ true - - - - - - - - - - - + + + + + + + + + + + @@ -23,9 +23,9 @@ - + - + \ No newline at end of file diff --git a/testenvironments.json b/testenvironments.json index e0b5a26..587b2ef 100644 --- a/testenvironments.json +++ b/testenvironments.json @@ -7,14 +7,9 @@ "wslDistribution": "Ubuntu-24.04" }, { - "name": "Docker-Ubuntu (net9)", + "name": "Docker-Ubuntu", "type": "docker", - "dockerImage": "codebeltnet/ubuntu-testrunner:9" - }, - { - "name": "Docker-Ubuntu (net10)", - "type": "docker", - "dockerImage": "codebeltnet/ubuntu-testrunner:10" + "dockerImage": "codebeltnet/ubuntu-testrunner:8-9-10-11" } ] }