Description
Description
The .NET configuration binder retrieves configuration values via configuration providers and attempts to bind these values to object properties. Previously, when a configuration value was null
, the binder treated it as if the value did not 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 used to convert 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 has been updated to treat 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 as well.
Version
.NET 10 Preview 7.
Previous behavior
Consider the following configuration file appsettings.json
:
{
"NullConfiguration": {
"StringProperty": null,
"IntProperty": null,
"Array1": [null, null],
"Array2": [],
},
}
And the corresponding binding code:
public class NullConfiguration
{
public NullConfiguration()
{
// Initialize with non-default value to ensure binding will override these values
StringProperty = "Initial Value";
IntProperty = 123;
}
public string? StringProperty1 { 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<NullConfiguration>();
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
: Remains unchanged (123) because the provider converts null to an empty string, which cannot be parsed as an int?
, so the original value is retained.
-Array1
: Binds to an array containing two empty strings because each null array element is treated as an empty string.
-Array2
: Remains null since an empty array []
in the JSON is ignored by the binder.
Now consider the same scenario using an in-memory configuration provider:
var inMemoryConfiguration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string?>
{
{ "NullConfiguration:StringProperty", null },
{ "NullConfiguration:IntProperty", null },
{ "NullConfiguration:Array1:0", null },
{ "NullConfiguration:Array1:1", null },
{ "NullConfiguration:Array2", "" },
}).Build().GetSection("NullConfiguration");
result = inMemoryConfiguration.Get<NullConfiguration>();
Console.WriteLine($"StringProperty: '{result!.StringProperty}', intProperty: {(result!.IntProperty.HasValue ? result!.IntProperty : "null")}");
Output:
StringProperty: 'Initial Value', intProperty: 123
Array1:
Array2: null
Explanation:
StringProperty
andIntProperty
: Remain unchanged because null values provided by the in-memory provider are treated as missing, and missing values do not overwrite existing property values.
-Array1
: Becomes an empty array, as all entries are null and thus ignored by the binder.
-Array2
: Remainsnull
because the null configuration value is treated as missing, and missing values do not overwrite existing property values.
New behavior
With the change, 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:
Using the In-Memory configuration provider:
StringProperty: 'null', intProperty: null
Array1: 'null', 'null'
Array2:
As you can see, null values are now properly bound to their corresponding properties, including array elements. Even the empty array is correctly recognized and bound as an empty array rather than being ignored.
Type of breaking change
- Binary incompatible: Existing binaries might encounter a breaking change in behavior, such as failure to load or execute, and if so, require recompilation.
- Source incompatible: When recompiled using the new SDK or component or to target the new runtime, existing source code might require source changes to compile successfully.
- Behavioral change: Existing binaries might behave differently at run time.
Reason for change
The previous behavior was clearly 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 users prefer the previous behavior for any reason, they can adjust their configuration accordingly.
- When using the JSON configuration provider, replacing
null
values with empty strings (""
) will restore the original behavior, where empty strings are bound instead ofnull
. - For other providers that support
null
values, simply removing thenull
entries from the configuration will replicate the earlier behavior, where missing values are ignored and existing property values remain unchanged.
Feature area
Extensions
Affected APIs
Metadata
Metadata
Assignees
Type
Projects
Status