Skip to content

Commit baec721

Browse files
authored
Support Null configuration (#116677)
* Support Null configuration * Remove the config switch * Fix configuration providers failing tests * Minor clean-up
1 parent 383f9af commit baec721

File tree

79 files changed

+3033
-1219
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+3033
-1219
lines changed

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs

Lines changed: 117 additions & 63 deletions
Large diffs are not rendered by default.

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/ExceptionMessages.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal static class ExceptionMessages
1313
public const string CannotBindToConstructorParameter = "Cannot create instance of type '{0}' because one or more parameters cannot be bound to. Constructor parameters cannot be declared as in, out, or ref. Invalid parameters are: '{1}'";
1414
public const string CannotSpecifyBindNonPublicProperties = "The configuration binding source generator does not support 'BinderOptions.BindNonPublicProperties'.";
1515
public const string ConstructorParametersDoNotMatchProperties = "Cannot create instance of type '{0}' because one or more parameters cannot be bound to. Constructor parameters must have corresponding properties. Fields are not supported. Missing properties are: '{1}'";
16-
public const string FailedBinding = "Failed to convert configuration value at '{0}' to type '{1}'.";
16+
public const string FailedBinding = "Failed to convert configuration value '{0}' at '{1}' to type '{2}'.";
1717
public const string MissingConfig = "'{0}' was set on the provided {1}, but the following properties were not found on the instance of {2}: {3}";
1818
public const string MissingPublicInstanceConstructor = "Cannot create instance of type '{0}' because it is missing a public instance constructor.";
1919
public const string MultipleParameterizedConstructors = "Cannot create instance of type '{0}' because it has multiple public parameterized constructors.";

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/Helpers.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ private static class Identifier
114114
public const string HasValue = nameof(HasValue);
115115
public const string IConfiguration = nameof(IConfiguration);
116116
public const string IConfigurationSection = nameof(IConfigurationSection);
117+
public const string ConfigurationSection = nameof(ConfigurationSection);
117118
public const string Int32 = "int";
118119
public const string InterceptsLocation = nameof(InterceptsLocation);
119120
public const string InvalidOperationException = nameof(InvalidOperationException);
@@ -133,6 +134,7 @@ private static class Identifier
133134
public const string Type = nameof(Type);
134135
public const string Uri = nameof(Uri);
135136
public const string ValidateConfigurationKeys = nameof(ValidateConfigurationKeys);
137+
public const string TryGetConfigurationValue = nameof(TryGetConfigurationValue);
136138
public const string Value = nameof(Value);
137139
}
138140

src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs

Lines changed: 62 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,8 @@ private static void BindProperty(PropertyInfo property, object instance, IConfig
311311
// For property binding, there are some cases when HasNewValue is not set in BindingPoint while a non-null Value inside that object can be retrieved from the property getter.
312312
// As example, when binding a property which not having a configuration entry matching this property and the getter can initialize the Value.
313313
// It is important to call the property setter as the setters can have a logic adjusting the Value.
314-
if (!propertyBindingPoint.IsReadOnly && propertyBindingPoint.Value is not null)
314+
// Otherwise, if the HasNewValue set to true, it means that the property setter should be called anyway as encountering a new value.
315+
if (!propertyBindingPoint.IsReadOnly && (propertyBindingPoint.Value is not null || propertyBindingPoint.HasNewValue))
315316
{
316317
property.SetValue(instance, propertyBindingPoint.Value);
317318
}
@@ -338,15 +339,41 @@ private static void BindInstance(
338339
return;
339340
}
340341

341-
var section = config as IConfigurationSection;
342-
string? configValue = section?.Value;
343-
if (configValue != null && TryConvertValue(type, configValue, section?.Path, out object? convertedValue, out Exception? error))
342+
IConfigurationSection? section;
343+
string? configValue;
344+
bool isConfigurationExist;
345+
346+
if (config is ConfigurationSection configSection)
347+
{
348+
section = configSection;
349+
isConfigurationExist = configSection.TryGetValue(key:null, out configValue);
350+
}
351+
else
352+
{
353+
section = config as IConfigurationSection;
354+
configValue = section?.Value;
355+
isConfigurationExist = configValue != null;
356+
}
357+
358+
if (isConfigurationExist && TryConvertValue(type, configValue, section?.Path, out object? convertedValue, out Exception? error))
344359
{
345360
if (error != null)
346361
{
347362
throw error;
348363
}
349364

365+
if (type == typeof(byte[]) && bindingPoint.Value is byte[] byteArray && byteArray.Length > 0)
366+
{
367+
if (convertedValue is byte[] convertedByteArray && convertedByteArray.Length > 0)
368+
{
369+
Array a = Array.CreateInstance(type.GetElementType()!, byteArray.Length + convertedByteArray.Length);
370+
Array.Copy(byteArray, a, byteArray.Length);
371+
Array.Copy(convertedByteArray, 0, a, byteArray.Length, convertedByteArray.Length);
372+
bindingPoint.TrySetValue(a);
373+
}
374+
return;
375+
}
376+
350377
// Leaf nodes are always reinitialized
351378
bindingPoint.TrySetValue(convertedValue);
352379
return;
@@ -476,13 +503,29 @@ private static void BindInstance(
476503
if (options.ErrorOnUnknownConfiguration)
477504
{
478505
Debug.Assert(section is not null);
479-
throw new InvalidOperationException(SR.Format(SR.Error_FailedBinding, section.Path, type));
506+
throw new InvalidOperationException(SR.Format(SR.Error_FailedBinding, configValue, section.Path, type));
480507
}
481508
}
482-
else if (isParentCollection && bindingPoint.Value is null)
509+
else
483510
{
484-
// Try to create the default instance of the type
485-
bindingPoint.TrySetValue(CreateInstance(type, config, options, out _));
511+
if (isParentCollection && bindingPoint.Value is null)
512+
{
513+
// Try to create the default instance of the type
514+
bindingPoint.TrySetValue(CreateInstance(type, config, options, out _));
515+
}
516+
else if (isConfigurationExist && bindingPoint.Value is null)
517+
{
518+
// Don't override the existing array in bindingPoint.Value if it is already set.
519+
if (type.IsArray || IsImmutableArrayCompatibleInterface(type))
520+
{
521+
// When having configuration value set to empty string, we create an empty array
522+
bindingPoint.TrySetValue(configValue is null ? null : Array.CreateInstance(type.GetElementType()!, 0));
523+
}
524+
else
525+
{
526+
bindingPoint.TrySetValue(bindingPoint.Value); // force setting null value
527+
}
528+
}
486529
}
487530
}
488531
}
@@ -930,7 +973,7 @@ private static Array BindArray(Type type, IEnumerable? source, IConfiguration co
930973
private static bool TryConvertValue(
931974
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
932975
Type type,
933-
string value, string? path, out object? result, out Exception? error)
976+
string? value, string? path, out object? result, out Exception? error)
934977
{
935978
error = null;
936979
result = null;
@@ -954,11 +997,14 @@ private static bool TryConvertValue(
954997
{
955998
try
956999
{
957-
result = converter.ConvertFromInvariantString(value);
1000+
if (value is not null)
1001+
{
1002+
result = converter.ConvertFromInvariantString(value);
1003+
}
9581004
}
9591005
catch (Exception ex)
9601006
{
961-
error = new InvalidOperationException(SR.Format(SR.Error_FailedBinding, path, type), ex);
1007+
error = new InvalidOperationException(SR.Format(SR.Error_FailedBinding, value, path, type), ex);
9621008
}
9631009
return true;
9641010
}
@@ -967,11 +1013,14 @@ private static bool TryConvertValue(
9671013
{
9681014
try
9691015
{
970-
result = Convert.FromBase64String(value);
1016+
if (value is not null )
1017+
{
1018+
result = value == string.Empty ? Array.Empty<byte>() : Convert.FromBase64String(value);
1019+
}
9711020
}
9721021
catch (FormatException ex)
9731022
{
974-
error = new InvalidOperationException(SR.Format(SR.Error_FailedBinding, path, type), ex);
1023+
error = new InvalidOperationException(SR.Format(SR.Error_FailedBinding, value, path, type), ex);
9751024
}
9761025
return true;
9771026
}

src/libraries/Microsoft.Extensions.Configuration.Binder/src/Microsoft.Extensions.Configuration.Binder.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
<ItemGroup>
3535
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Configuration.Abstractions\src\Microsoft.Extensions.Configuration.Abstractions.csproj" />
36+
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Configuration\src\Microsoft.Extensions.Configuration.csproj" />
3637
<ProjectReference Include="..\gen\Microsoft.Extensions.Configuration.Binder.SourceGeneration.csproj"
3738
ReferenceOutputAssembly="false"
3839
PackAsAnalyzer="true" />

src/libraries/Microsoft.Extensions.Configuration.Binder/src/Resources/Strings.resx

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<?xml version="1.0" encoding="utf-8"?>
22
<root>
3-
<!--
4-
Microsoft ResX Schema
5-
3+
<!--
4+
Microsoft ResX Schema
5+
66
Version 2.0
7-
8-
The primary goals of this format is to allow a simple XML format
9-
that is mostly human readable. The generation and parsing of the
10-
various data types are done through the TypeConverter classes
7+
8+
The primary goals of this format is to allow a simple XML format
9+
that is mostly human readable. The generation and parsing of the
10+
various data types are done through the TypeConverter classes
1111
associated with the data types.
12-
12+
1313
Example:
14-
14+
1515
... ado.net/XML headers & schema ...
1616
<resheader name="resmimetype">text/microsoft-resx</resheader>
1717
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
2626
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
2727
<comment>This is a comment</comment>
2828
</data>
29-
30-
There are any number of "resheader" rows that contain simple
29+
30+
There are any number of "resheader" rows that contain simple
3131
name/value pairs.
32-
33-
Each data row contains a name, and value. The row also contains a
34-
type or mimetype. Type corresponds to a .NET class that support
35-
text/value conversion through the TypeConverter architecture.
36-
Classes that don't support this are serialized and stored with the
32+
33+
Each data row contains a name, and value. The row also contains a
34+
type or mimetype. Type corresponds to a .NET class that support
35+
text/value conversion through the TypeConverter architecture.
36+
Classes that don't support this are serialized and stored with the
3737
mimetype set.
38-
39-
The mimetype is used for serialized objects, and tells the
40-
ResXResourceReader how to depersist the object. This is currently not
38+
39+
The mimetype is used for serialized objects, and tells the
40+
ResXResourceReader how to depersist the object. This is currently not
4141
extensible. For a given mimetype the value must be set accordingly:
42-
43-
Note - application/x-microsoft.net.object.binary.base64 is the format
44-
that the ResXResourceWriter will generate, however the reader can
42+
43+
Note - application/x-microsoft.net.object.binary.base64 is the format
44+
that the ResXResourceWriter will generate, however the reader can
4545
read any of the formats listed below.
46-
46+
4747
mimetype: application/x-microsoft.net.object.binary.base64
48-
value : The object must be serialized with
48+
value : The object must be serialized with
4949
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
5050
: and then encoded with base64 encoding.
51-
51+
5252
mimetype: application/x-microsoft.net.object.soap.base64
53-
value : The object must be serialized with
53+
value : The object must be serialized with
5454
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
5555
: and then encoded with base64 encoding.
5656
5757
mimetype: application/x-microsoft.net.object.bytearray.base64
58-
value : The object must be serialized into a byte array
58+
value : The object must be serialized into a byte array
5959
: using a System.ComponentModel.TypeConverter
6060
: and then encoded with base64 encoding.
6161
-->
@@ -127,7 +127,7 @@
127127
<value>Cannot create instance of type '{0}' because one or more parameters cannot be bound to. Constructor parameters must have corresponding properties. Fields are not supported. Missing properties are: '{1}'</value>
128128
</data>
129129
<data name="Error_FailedBinding" xml:space="preserve">
130-
<value>Failed to convert configuration value at '{0}' to type '{1}'.</value>
130+
<value>Failed to convert configuration value '{0}' at '{1}' to type '{2}'.</value>
131131
</data>
132132
<data name="Error_FailedToActivate" xml:space="preserve">
133133
<value>Failed to create instance of type '{0}'.</value>
@@ -153,4 +153,4 @@
153153
<data name="Error_UnsupportedMultidimensionalArray" xml:space="preserve">
154154
<value>Cannot create instance of type '{0}' because multidimensional arrays are not supported.</value>
155155
</data>
156-
</root>
156+
</root>

src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Collections.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public void GetListNullValues()
6161
var list = new List<string>();
6262
config.GetSection("StringList").Bind(list);
6363

64-
Assert.Empty(list);
64+
Assert.Equal([ null, null, null, null ], list);
6565
}
6666

6767
[ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure exception messages are in sync
@@ -2182,9 +2182,10 @@ public void CanBindInstantiatedIEnumerableWithNullItems()
21822182

21832183
var options = config.Get<ComplexOptions>()!;
21842184

2185-
Assert.Equal(2, options.InstantiatedIEnumerable.Count());
2186-
Assert.Equal("Yo1", options.InstantiatedIEnumerable.ElementAt(0));
2187-
Assert.Equal("Yo2", options.InstantiatedIEnumerable.ElementAt(1));
2185+
Assert.Equal(3, options.InstantiatedIEnumerable.Count());
2186+
Assert.Null(options.InstantiatedIEnumerable.ElementAt(0));
2187+
Assert.Equal("Yo1", options.InstantiatedIEnumerable.ElementAt(1));
2188+
Assert.Equal("Yo2", options.InstantiatedIEnumerable.ElementAt(2));
21882189
}
21892190

21902191
[Fact]

src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,5 +1147,36 @@ public enum MyValue
11471147
Value2,
11481148
Value3
11491149
}
1150+
1151+
public class NullConfiguration
1152+
{
1153+
public NullConfiguration()
1154+
{
1155+
// Initialize with non-default value to ensure binding will override these values
1156+
StringProperty1 = "Initial Value 1";
1157+
StringProperty2 = "Initial Value 2";
1158+
StringProperty3 = "Initial Value 3";
1159+
1160+
IntProperty1 = 123;
1161+
IntProperty2 = 456;
1162+
}
1163+
public string? StringProperty1 { get; set; }
1164+
public string? StringProperty2 { get; set; }
1165+
public string? StringProperty3 { get; set; }
1166+
1167+
public int? IntProperty1 { get; set; }
1168+
public int? IntProperty2 { get; set; }
1169+
}
1170+
1171+
public class ArraysContainer
1172+
{
1173+
public string[] StringArray1 { get; set; }
1174+
public string[] StringArray2 { get; set; }
1175+
public string[] StringArray3 { get; set; }
1176+
1177+
public byte[] ByteArray1 { get; set; }
1178+
public byte[] ByteArray2 { get; set; }
1179+
public byte[] ByteArray3 { get; set; }
1180+
}
11501181
}
11511182
}

0 commit comments

Comments
 (0)