From 2a2aac68598989c1f473aa4fa6eb5af473edb487 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 23:37:29 +0000 Subject: [PATCH 1/3] Initial plan From ac10e7ef6a03cc78324d7175a476716721260bf7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 5 Aug 2025 23:46:14 +0000 Subject: [PATCH 2/3] Add breaking change documentation for configuration null values preservation Co-authored-by: gewarren <24882762+gewarren@users.noreply.github.com> --- docs/core/compatibility/10.0.md | 1 + .../configuration-null-values-preserved.md | 115 ++++++++++++++++++ docs/core/compatibility/toc.yml | 2 + 3 files changed, 118 insertions(+) create mode 100644 docs/core/compatibility/extensions/10.0/configuration-null-values-preserved.md diff --git a/docs/core/compatibility/10.0.md b/docs/core/compatibility/10.0.md index 1b71ea787cc16..432e80a020f36 100644 --- a/docs/core/compatibility/10.0.md +++ b/docs/core/compatibility/10.0.md @@ -41,6 +41,7 @@ If you're migrating an app to .NET 10, the breaking changes listed here might af | Title | Type of change | Introduced version | |-------|---------------------|--------------------| +| [Preserving null values in configuration](extensions/10.0/configuration-null-values-preserved.md) | Behavioral change | Preview 7 | | [ProviderAliasAttribute moved to Microsoft.Extensions.Logging.Abstractions assembly](extensions/10.0/provideraliasattribute-moved-assembly.md) | Source incompatible | Preview 4 | | [Removed DynamicallyAccessedMembers annotation from trim-unsafe Microsoft.Extensions.Configuration code](extensions/10.0/dynamically-accessed-members-configuration.md) | Binary incompatible | Preview 6 | diff --git a/docs/core/compatibility/extensions/10.0/configuration-null-values-preserved.md b/docs/core/compatibility/extensions/10.0/configuration-null-values-preserved.md new file mode 100644 index 0000000000000..b21310ea7df6d --- /dev/null +++ b/docs/core/compatibility/extensions/10.0/configuration-null-values-preserved.md @@ -0,0 +1,115 @@ +--- +title: "Breaking change: Preserving null values in configuration" +description: "Learn about the breaking change in .NET 10 where configuration providers now preserve null values instead of treating them as missing values." +ms.date: 12/17/2024 +ai-usage: ai-assisted +ms.custom: https://github.com/dotnet/docs/issues/46890 +--- + +# Preserving null values in configuration + +The .NET configuration binder now preserves null values in configuration instead of treating them as missing values. This change affects how the JSON configuration provider handles null values and how the configuration binder processes them during binding operations. + +## Version introduced + +.NET 10 Preview 7 + +## Previous behavior + +Previously, when a configuration value was `null`, the binder treated it as if the value didn't exist at all, and therefore skipped the binding. The system didn't distinguish between `null` values and missing values. + +Additionally, the JSON configuration provider converted `null` values in the configuration to empty strings. This caused properties bound to these values to receive an empty string rather than the expected `null`. + +Consider the following configuration file `appsettings.json`: + +```json +{ + "NullConfiguration": { + "StringProperty": null, + "IntProperty": null, + "Array1": [null, null], + "Array2": [] + } +} +``` + +And the corresponding binding code: + +```csharp +public class NullConfiguration +{ + public NullConfiguration() + { + // Initialize with non-default value to ensure binding will override these values + StringProperty = "Initial Value"; + IntProperty = 123; + } + public string? StringProperty { get; set; } + public int? IntProperty { get; set; } + public string[]? Array1 { get; set; } + public string[]? Array2 { get; set; } +} + +var configuration = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .Build().GetSection("NullConfiguration"); + +// Now bind the configuration +NullConfiguration? result = configuration.Get(); + +Console.WriteLine($"StringProperty: '{result!.StringProperty}', intProperty: {(result!.IntProperty.HasValue ? result!.IntProperty : "null")}"); +Console.WriteLine($"Array1: {(result!.Array1 is null ? "null" : string.Join(", ", result!.Array1.Select(a => $"'{(a is null ? "null" : a)}'")))}"); +Console.WriteLine($"Array2: {(result!.Array2 is null ? "null" : string.Join(", ", result!.Array2.Select(a => $"'{(a is null ? "null" : a)}'")))}"); +``` + +Output: + +``` +StringProperty: '', intProperty: 123 +Array1: '', '' +Array2: null +``` + +Explanation: +- `StringProperty`: The null value in the JSON was converted by the JSON provider into an empty string (""), overwriting the initial value. +- `IntProperty`: Remained unchanged (123) because the provider converted null to an empty string, which couldn't be parsed as an `int?`, so the original value was retained. +- `Array1`: Bound to an array containing two empty strings because each null array element was treated as an empty string. +- `Array2`: Remained null since an empty array `[]` in the JSON was ignored by the binder. + +## New behavior + +Null values in the configuration are now correctly honored. Running the same code sample produces the following results: + +Using the JSON configuration provider: + +``` +StringProperty: 'null', intProperty: null +Array1: 'null', 'null' +Array2: +``` + +Null values are now properly bound to their corresponding properties, including array elements. Even empty arrays are correctly recognized and bound as empty arrays rather than being ignored. + +## Type of breaking change + +This is a [behavioral change](../../categories.md#behavioral-change). + +## Reason for change + +The previous behavior was confusing and frequently led to user complaints. By addressing this issue, the configuration binding process is now more intuitive and consistent, reducing confusion and aligning the behavior with user expectations. + +## Recommended action + +If you prefer the previous behavior, you can adjust your configuration accordingly: + +- When using the **JSON configuration provider**, replace `null` values with empty strings (`""`) to restore the original behavior, where empty strings are bound instead of `null`. +- For other providers that support `null` values, simply **remove the `null` entries** from the configuration to replicate the earlier behavior, where missing values are ignored and existing property values remain unchanged. + +## Affected APIs + +- +- +- +- +- +- \ No newline at end of file diff --git a/docs/core/compatibility/toc.yml b/docs/core/compatibility/toc.yml index bef069b8a508c..f7a341547755f 100644 --- a/docs/core/compatibility/toc.yml +++ b/docs/core/compatibility/toc.yml @@ -50,6 +50,8 @@ items: href: cryptography/10.0/x509-publickey-null.md - name: Extensions items: + - name: "Preserving null values in configuration" + href: extensions/10.0/configuration-null-values-preserved.md - name: "ProviderAliasAttribute moved to Microsoft.Extensions.Logging.Abstractions assembly" href: extensions/10.0/provideraliasattribute-moved-assembly.md - name: "Removed DynamicallyAccessedMembers annotation from trim-unsafe Microsoft.Extensions.Configuration code" From 31d32f81fa331e0170b03657fb7e4671795cb487 Mon Sep 17 00:00:00 2001 From: Genevieve Warren <24882762+gewarren@users.noreply.github.com> Date: Thu, 7 Aug 2025 13:32:15 -0700 Subject: [PATCH 3/3] human edits --- docs/core/compatibility/10.0.md | 2 +- .../configuration-null-values-preserved.md | 59 ++++++++++--------- docs/core/compatibility/toc.yml | 2 +- 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/docs/core/compatibility/10.0.md b/docs/core/compatibility/10.0.md index 432e80a020f36..66cba43ea1e76 100644 --- a/docs/core/compatibility/10.0.md +++ b/docs/core/compatibility/10.0.md @@ -41,7 +41,7 @@ If you're migrating an app to .NET 10, the breaking changes listed here might af | Title | Type of change | Introduced version | |-------|---------------------|--------------------| -| [Preserving null values in configuration](extensions/10.0/configuration-null-values-preserved.md) | Behavioral change | Preview 7 | +| [Null values preserved in configuration](extensions/10.0/configuration-null-values-preserved.md) | Behavioral change | Preview 7 | | [ProviderAliasAttribute moved to Microsoft.Extensions.Logging.Abstractions assembly](extensions/10.0/provideraliasattribute-moved-assembly.md) | Source incompatible | Preview 4 | | [Removed DynamicallyAccessedMembers annotation from trim-unsafe Microsoft.Extensions.Configuration code](extensions/10.0/dynamically-accessed-members-configuration.md) | Binary incompatible | Preview 6 | diff --git a/docs/core/compatibility/extensions/10.0/configuration-null-values-preserved.md b/docs/core/compatibility/extensions/10.0/configuration-null-values-preserved.md index b21310ea7df6d..aea7ee007a2e4 100644 --- a/docs/core/compatibility/extensions/10.0/configuration-null-values-preserved.md +++ b/docs/core/compatibility/extensions/10.0/configuration-null-values-preserved.md @@ -1,14 +1,20 @@ --- -title: "Breaking change: Preserving null values in configuration" +title: "Breaking change: Null values preserved in configuration" description: "Learn about the breaking change in .NET 10 where configuration providers now preserve null values instead of treating them as missing values." -ms.date: 12/17/2024 +ms.date: 08/07/2025 ai-usage: ai-assisted ms.custom: https://github.com/dotnet/docs/issues/46890 --- -# Preserving null values in configuration +# Null values preserved in configuration -The .NET configuration binder now preserves null values in configuration instead of treating them as missing values. This change affects how the JSON configuration provider handles null values and how the configuration binder processes them during binding operations. +The .NET configuration binder retrieves configuration values via configuration providers and attempts to bind those values to object properties. Previously, when a configuration value was null, the binder treated it as if the value didn't exist at all, and therefore skipped the binding. In other words, it did not distinguish between `null` values and missing values. This behavior caused significant confusion for users who expected explicitly defined `null` values in their configuration to be respected and properly bound. + +Additionally, the JSON configuration provider previously converted `null` values in the configuration to empty strings. This further contributed to confusion, as properties bound to these values would receive an empty string rather than the expected null. + +This change addresses both issues. The JSON configuration provider now correctly reports `null` values without altering them, and the binder treats `null` values as valid inputs, binding them like any other value. + +The update also includes improvements to support binding `null` values within arrays and enables binding of empty arrays. ## Version introduced @@ -20,7 +26,7 @@ Previously, when a configuration value was `null`, the binder treated it as if t Additionally, the JSON configuration provider converted `null` values in the configuration to empty strings. This caused properties bound to these values to receive an empty string rather than the expected `null`. -Consider the following configuration file `appsettings.json`: +Consider the following configuration file `appsettings.json` contents: ```json { @@ -40,7 +46,8 @@ public class NullConfiguration { public NullConfiguration() { - // Initialize with non-default value to ensure binding will override these values + // Initialize with non-default value to + // ensure binding overrides these values. StringProperty = "Initial Value"; IntProperty = 123; } @@ -54,42 +61,43 @@ var configuration = new ConfigurationBuilder() .AddJsonFile("appsettings.json") .Build().GetSection("NullConfiguration"); -// Now bind the configuration +// Now bind the configuration. NullConfiguration? result = configuration.Get(); Console.WriteLine($"StringProperty: '{result!.StringProperty}', intProperty: {(result!.IntProperty.HasValue ? result!.IntProperty : "null")}"); -Console.WriteLine($"Array1: {(result!.Array1 is null ? "null" : string.Join(", ", result!.Array1.Select(a => $"'{(a is null ? "null" : a)}'")))}"); -Console.WriteLine($"Array2: {(result!.Array2 is null ? "null" : string.Join(", ", result!.Array2.Select(a => $"'{(a is null ? "null" : a)}'")))}"); +Console.WriteLine($"Array1: {(result!.Array1 is null ? + "null" : string.Join(", ", result!.Array1.Select(a => $"'{(a is null ? "null" : a)}'")))}"); +Console.WriteLine($"Array2: {(result!.Array2 is null ? + "null" : string.Join(", ", result!.Array2.Select(a => $"'{(a is null ? "null" : a)}'")))}"); ``` Output: -``` +```txt StringProperty: '', intProperty: 123 Array1: '', '' Array2: null ``` -Explanation: -- `StringProperty`: The null value in the JSON was converted by the JSON provider into an empty string (""), overwriting the initial value. -- `IntProperty`: Remained unchanged (123) because the provider converted null to an empty string, which couldn't be parsed as an `int?`, so the original value was retained. -- `Array1`: Bound to an array containing two empty strings because each null array element was treated as an empty string. -- `Array2`: Remained null since an empty array `[]` in the JSON was ignored by the binder. +Explanation of the output: + +- `StringProperty`: The `null` value in the JSON was converted by the JSON provider into an empty string (""), overwriting the initial value. +- `IntProperty`: Remained unchanged (123) because the provider converted `null` to an empty string, which couldn't be parsed as an `int?`, so the original value was retained. +- `Array1`: Bound to an array containing two empty strings because each `null` array element was treated as an empty string. +- `Array2`: Remained `null` since an empty array `[]` in the JSON was ignored by the binder. ## New behavior -Null values in the configuration are now correctly honored. Running the same code sample produces the following results: +Starting in .NET 10, `null` values are now properly bound to their corresponding properties, including array elements. Even empty arrays are correctly recognized and bound as empty arrays rather than being ignored. -Using the JSON configuration provider: +Running the same code sample produces the following results using the JSON configuration provider: -``` +```txt StringProperty: 'null', intProperty: null Array1: 'null', 'null' Array2: ``` -Null values are now properly bound to their corresponding properties, including array elements. Even empty arrays are correctly recognized and bound as empty arrays rather than being ignored. - ## Type of breaking change This is a [behavioral change](../../categories.md#behavioral-change). @@ -102,14 +110,9 @@ The previous behavior was confusing and frequently led to user complaints. By ad If you prefer the previous behavior, you can adjust your configuration accordingly: -- When using the **JSON configuration provider**, replace `null` values with empty strings (`""`) to restore the original behavior, where empty strings are bound instead of `null`. -- For other providers that support `null` values, simply **remove the `null` entries** from the configuration to replicate the earlier behavior, where missing values are ignored and existing property values remain unchanged. +- When using the JSON configuration provider, replace `null` values with empty strings (`""`) to restore the original behavior, where empty strings are bound instead of `null`. +- For other providers that support `null` values, remove the `null` entries from the configuration to replicate the earlier behavior, where missing values are ignored and existing property values remain unchanged. ## Affected APIs -- -- -- -- -- -- \ No newline at end of file +- APIs diff --git a/docs/core/compatibility/toc.yml b/docs/core/compatibility/toc.yml index f7a341547755f..fcee64df7b649 100644 --- a/docs/core/compatibility/toc.yml +++ b/docs/core/compatibility/toc.yml @@ -50,7 +50,7 @@ items: href: cryptography/10.0/x509-publickey-null.md - name: Extensions items: - - name: "Preserving null values in configuration" + - name: Null values preserved in configuration href: extensions/10.0/configuration-null-values-preserved.md - name: "ProviderAliasAttribute moved to Microsoft.Extensions.Logging.Abstractions assembly" href: extensions/10.0/provideraliasattribute-moved-assembly.md