Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -976,11 +976,13 @@ complexType is not CollectionSpec &&

// 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.
// In this case, we will assign an empty array to the member. Otherwise, we will skip the binding logic.
if (complexType is ArraySpec arraySpec && canSet)
if ((complexType is ArraySpec || complexType.IsExactIEnumerableOfT) && canSet)
{
// 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.
Debug.Assert(complexType is ArraySpec || complexType is EnumerableSpec);
string valueIdentifier = GetIncrementalIdentifier(Identifier.value);
EmitStartBlock($@"if ({memberAccessExpr} is null && {Identifier.TryGetConfigurationValue}({configSection}, {Identifier.key}: null, out string? {valueIdentifier}) && {valueIdentifier} == string.Empty)");
_writer.WriteLine($"{memberAccessExpr} = global::System.{Identifier.Array}.Empty<{arraySpec.ElementTypeRef.FullyQualifiedName}>();");
_writer.WriteLine($"{memberAccessExpr} = global::System.{Identifier.Array}.Empty<{((CollectionSpec)complexType).ElementTypeRef.FullyQualifiedName}>();");
EmitEndBlock();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public KnownTypeSymbols(CSharpCompilation compilation)
Uri = compilation.GetBestTypeByMetadataName(typeof(Uri));
Version = compilation.GetBestTypeByMetadataName(typeof(Version));

// Used to verify input configuation binding API calls.
// Used to verify input configuration binding API calls.
INamedTypeSymbol? binderOptions = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Configuration.BinderOptions");
ActionOfBinderOptions = binderOptions is null ? null : compilation.GetBestTypeByMetadataName(typeof(Action<>))?.Construct(binderOptions);
ConfigurationBinder = compilation.GetBestTypeByMetadataName("Microsoft.Extensions.Configuration.ConfigurationBinder");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ public TypeSpec(ITypeSymbol type)
(DisplayString, FullName) = type.GetTypeNames();
IdentifierCompatibleSubstring = type.ToIdentifierCompatibleSubstring();
IsValueType = type.IsValueType;
IsValueTuple = type is INamedTypeSymbol namedTypeSymbol && namedTypeSymbol.IsTupleType;

if (type is INamedTypeSymbol namedTypeSymbol)
{
IsValueTuple = namedTypeSymbol.IsTupleType;
IsExactIEnumerableOfT = namedTypeSymbol.OriginalDefinition.SpecialType == SpecialType.System_Collections_Generic_IEnumerable_T;
}
}

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

public bool IsValueTuple { get; }

public bool IsExactIEnumerableOfT { get; }
}

public abstract record ComplexTypeSpec : TypeSpec
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -503,20 +503,19 @@ private static void BindInstance(
throw new InvalidOperationException(SR.Format(SR.Error_FailedBinding, configValue, section.Path, type));
}
}
else
else if (!bindingPoint.IsReadOnly && bindingPoint.Value is null)
{
if (isParentCollection && bindingPoint.Value is null)
if (isParentCollection)
{
// Try to create the default instance of the type
bindingPoint.TrySetValue(CreateInstance(type, config, options, out _));
}
else if (isConfigurationExist && bindingPoint.Value is null)
else if (isConfigurationExist)
{
// Don't override the existing array in bindingPoint.Value if it is already set.
if (type.IsArray || IsImmutableArrayCompatibleInterface(type))
if (type.IsArray || IsIEnumerableInterface(type))
{
// When having configuration value set to empty string, we create an empty array
bindingPoint.TrySetValue(configValue is null ? null : Array.CreateInstance(type.GetElementType()!, 0));
bindingPoint.TrySetValue(configValue is null ? null : Array.CreateInstance(type.IsArray ? type.GetElementType()! : type.GetGenericArguments()[0], 0));
}
else
{
Expand Down Expand Up @@ -1056,6 +1055,9 @@ private static bool IsImmutableArrayCompatibleInterface(Type type)
|| genericTypeDefinition == typeof(IReadOnlyList<>);
}

private static bool IsIEnumerableInterface(Type type)
=> type.IsInterface && type.IsConstructedGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);

private static bool TypeIsASetInterface(Type type)
{
if (!type.IsInterface || !type.IsConstructedGenericType) { return false; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1178,5 +1178,11 @@ public class ArraysContainer
public byte[] ByteArray2 { get; set; }
public byte[] ByteArray3 { get; set; }
}

public class MyOptionsWithNullableEnumerable
{
public IEnumerable<int>? IEnumerableProperty { get; set; }
public string[] StringArray { get; set; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3038,5 +3038,27 @@ public void TestProvidersOrder()
Assert.Equal("Provider2A", result.A); // Value should come from the last provider
Assert.Equal("Provider1B", result.B); // B should not be overridden by the second provider
}

[Fact]
public void TestBindingEmptyArrayToNullIEnumerable()
{
string jsonConfig1 = @"
{
""MyService"": {
""IEnumerableProperty"": [],
""StringArray"": []
},
}";

var configuration = new ConfigurationBuilder()
.AddJsonStream(new MemoryStream(Encoding.UTF8.GetBytes(jsonConfig1)))
.Build().GetSection("MyService");

MyOptionsWithNullableEnumerable? result = configuration.Get<MyOptionsWithNullableEnumerable>();

Assert.NotNull(result);
Assert.Equal(Array.Empty<int>(), result.IEnumerableProperty);
Assert.Equal(Array.Empty<string>(), result.StringArray);
}
}
}
Loading