Skip to content

Commit 765b715

Browse files
[release/10.0] Fix binding IEnumerable<T> with empty array configuration (#121325)
Backport of #121249 to release/10.0 /cc @tarekgh ## Customer Impact - [x] Customer reported - [ ] Found internally Applications that use an empty array configuration such as `"IEnumerableProperty": []` and bind it to an uninitialized property of type `IEnumerable<T>`, `IReadOnlyList<T>`, or `IReadOnlyCollection<T>` will encounter an `ArgumentNullException`. This exception can cause the application to crash if it isn’t properly handled. The issue is reported by the issue #121249 ## Regression - [x] Yes - [ ] No The regression was introduced in .NET 10 Preview 7 through PR #116677. ## Testing Tested all potential failure cases manually and ensured no exceptions are thrown. Also added a regression test for the previously failing scenario. ## Risk Low, as the fix was scoped to only affect the specific failing scenario, when an empty list configuration is bound to a property of the specified type. Co-authored-by: Tarek Mahmoud Sayed <tarekms@microsoft.com>
1 parent ec71a92 commit 765b715

File tree

6 files changed

+49
-10
lines changed

6 files changed

+49
-10
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -976,11 +976,13 @@ complexType is not CollectionSpec &&
976976

977977
// The current configuration section doesn't have any children, let's check if we are binding to an array and the configuration value is empty string.
978978
// In this case, we will assign an empty array to the member. Otherwise, we will skip the binding logic.
979-
if (complexType is ArraySpec arraySpec && canSet)
979+
if ((complexType is ArraySpec || complexType.IsExactIEnumerableOfT) && canSet)
980980
{
981+
// Either we have an array or we have an IEnumerable<T> both these types can be assigned an empty array when having empty string configuration value.
982+
Debug.Assert(complexType is ArraySpec || complexType is EnumerableSpec);
981983
string valueIdentifier = GetIncrementalIdentifier(Identifier.value);
982984
EmitStartBlock($@"if ({memberAccessExpr} is null && {Identifier.TryGetConfigurationValue}({configSection}, {Identifier.key}: null, out string? {valueIdentifier}) && {valueIdentifier} == string.Empty)");
983-
_writer.WriteLine($"{memberAccessExpr} = global::System.{Identifier.Array}.Empty<{arraySpec.ElementTypeRef.FullyQualifiedName}>();");
985+
_writer.WriteLine($"{memberAccessExpr} = global::System.{Identifier.Array}.Empty<{((CollectionSpec)complexType).ElementTypeRef.FullyQualifiedName}>();");
984986
EmitEndBlock();
985987
}
986988

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Parser/KnownTypeSymbols.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ public KnownTypeSymbols(CSharpCompilation compilation)
8484
Uri = compilation.GetBestTypeByMetadataName(typeof(Uri));
8585
Version = compilation.GetBestTypeByMetadataName(typeof(Version));
8686

87-
// Used to verify input configuation binding API calls.
87+
// Used to verify input configuration binding API calls.
8888
INamedTypeSymbol? binderOptions = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Configuration.BinderOptions");
8989
ActionOfBinderOptions = binderOptions is null ? null : compilation.GetBestTypeByMetadataName(typeof(Action<>))?.Construct(binderOptions);
9090
ConfigurationBinder = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Configuration.ConfigurationBinder");

src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/TypeSpec.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ public TypeSpec(ITypeSymbol type)
1717
(DisplayString, FullName) = type.GetTypeNames();
1818
IdentifierCompatibleSubstring = type.ToIdentifierCompatibleSubstring();
1919
IsValueType = type.IsValueType;
20-
IsValueTuple = type is INamedTypeSymbol namedTypeSymbol && namedTypeSymbol.IsTupleType;
20+
21+
if (type is INamedTypeSymbol namedTypeSymbol)
22+
{
23+
IsValueTuple = namedTypeSymbol.IsTupleType;
24+
IsExactIEnumerableOfT = namedTypeSymbol.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T;
25+
}
2126
}
2227

2328
public TypeRef TypeRef { get; }
@@ -39,6 +44,8 @@ public TypeSpec(ITypeSymbol type)
3944
public bool IsValueType { get; }
4045

4146
public bool IsValueTuple { get; }
47+
48+
public bool IsExactIEnumerableOfT { get; }
4249
}
4350

4451
public abstract record ComplexTypeSpec : TypeSpec

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -503,20 +503,19 @@ private static void BindInstance(
503503
throw new InvalidOperationException(SR.Format(SR.Error_FailedBinding, configValue, section.Path, type));
504504
}
505505
}
506-
else
506+
else if (!bindingPoint.IsReadOnly && bindingPoint.Value is null)
507507
{
508-
if (isParentCollection && bindingPoint.Value is null)
508+
if (isParentCollection)
509509
{
510510
// Try to create the default instance of the type
511511
bindingPoint.TrySetValue(CreateInstance(type, config, options, out _));
512512
}
513-
else if (isConfigurationExist && bindingPoint.Value is null)
513+
else if (isConfigurationExist)
514514
{
515-
// Don't override the existing array in bindingPoint.Value if it is already set.
516-
if (type.IsArray || IsImmutableArrayCompatibleInterface(type))
515+
if (type.IsArray || IsIEnumerableInterface(type))
517516
{
518517
// When having configuration value set to empty string, we create an empty array
519-
bindingPoint.TrySetValue(configValue is null ? null : Array.CreateInstance(type.GetElementType()!, 0));
518+
bindingPoint.TrySetValue(configValue is null ? null : Array.CreateInstance(type.IsArray ? type.GetElementType()! : type.GetGenericArguments()[0], 0));
520519
}
521520
else
522521
{
@@ -1056,6 +1055,9 @@ private static bool IsImmutableArrayCompatibleInterface(Type type)
10561055
|| genericTypeDefinition == typeof(IReadOnlyList<>);
10571056
}
10581057

1058+
private static bool IsIEnumerableInterface(Type type)
1059+
=> type.IsInterface && type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
1060+
10591061
private static bool TypeIsASetInterface(Type type)
10601062
{
10611063
if (!type.IsInterface || !type.IsConstructedGenericType) { return false; }

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1178,5 +1178,11 @@ public class ArraysContainer
11781178
public byte[] ByteArray2 { get; set; }
11791179
public byte[] ByteArray3 { get; set; }
11801180
}
1181+
1182+
public class MyOptionsWithNullableEnumerable
1183+
{
1184+
public IEnumerable<int>? IEnumerableProperty { get; set; }
1185+
public string[] StringArray { get; set; }
1186+
}
11811187
}
11821188
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3038,5 +3038,27 @@ public void TestProvidersOrder()
30383038
Assert.Equal("Provider2A", result.A); // Value should come from the last provider
30393039
Assert.Equal("Provider1B", result.B); // B should not be overridden by the second provider
30403040
}
3041+
3042+
[Fact]
3043+
public void TestBindingEmptyArrayToNullIEnumerable()
3044+
{
3045+
string jsonConfig1 = @"
3046+
{
3047+
""MyService"": {
3048+
""IEnumerableProperty"": [],
3049+
""StringArray"": []
3050+
},
3051+
}";
3052+
3053+
var configuration = new ConfigurationBuilder()
3054+
.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(jsonConfig1)))
3055+
.Build().GetSection("MyService");
3056+
3057+
MyOptionsWithNullableEnumerable? result = configuration.Get<MyOptionsWithNullableEnumerable>();
3058+
3059+
Assert.NotNull(result);
3060+
Assert.Equal(Array.Empty<int>(), result.IEnumerableProperty);
3061+
Assert.Equal(Array.Empty<string>(), result.StringArray);
3062+
}
30413063
}
30423064
}

0 commit comments

Comments
 (0)