Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -19,6 +19,7 @@ public static class ConfigurationBinder
private const string TrimmingWarningMessage = "In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.";
private const string InstanceGetTypeTrimmingWarningMessage = "Cannot statically analyze the type of instance so its members may be trimmed";
private const string PropertyTrimmingWarningMessage = "Cannot statically analyze property.PropertyType so its members may be trimmed.";
private const string BindSingleElementsToArraySwitch = "Microsoft.Extensions.Configuration.BindSingleElementsToArray";

/// <summary>
/// Attempts to bind the configuration instance to a new instance of type T.
Expand Down Expand Up @@ -362,7 +363,7 @@ private static object BindInstance(
return convertedValue;
}

if (config != null && config.GetChildren().Any())
if (config != null && (config.GetChildren().Any() || (configValue != null && ShouldBindSingleElementsToArray())))
{
// If we don't have an instance, try to create one
if (instance == null)
Expand Down Expand Up @@ -495,7 +496,7 @@ private static void BindCollection(
Type itemType = collectionType.GenericTypeArguments[0];
MethodInfo addMethod = collectionType.GetMethod("Add", DeclaredOnlyLookup);

foreach (IConfigurationSection section in config.GetChildren())
foreach (IConfigurationSection section in GetChildrenOrSelf(config))
{
try
{
Expand All @@ -518,7 +519,7 @@ private static void BindCollection(
[RequiresUnreferencedCode("Cannot statically analyze what the element type is of the Array so its members may be trimmed.")]
private static Array BindArray(Array source, IConfiguration config, BinderOptions options)
{
IConfigurationSection[] children = config.GetChildren().ToArray();
IConfigurationSection[] children = GetChildrenOrSelf(config).ToArray();
int arrayLength = source.Length;
Type elementType = source.GetType().GetElementType();
var newArray = Array.CreateInstance(elementType, arrayLength + children.Length);
Expand Down Expand Up @@ -702,5 +703,38 @@ private static string GetPropertyName(MemberInfo property)

return property.Name;
}

private static IEnumerable<IConfigurationSection> GetChildrenOrSelf(IConfiguration config)
{
if (!ShouldBindSingleElementsToArray())
{
return config.GetChildren();
}


IEnumerable<IConfigurationSection> children;
// If configuration's children is an array, the configuration key will be a number
if (config.GetChildren().Any(a => long.TryParse(a.Key, out _)))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HaoK let us know if you know of a better way for this condition

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If any of the children is an integer (key:999), this assumes the actual object is an array.

In real world, either all children are integers or all are not. But doing "Any" prevents usages in weird cases where someone used both things
having key:a, key:0 doesn't make sense but i am assuming that it is an array (as in the old behavior)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the old behavior always used the children correct? Why does this need to check if any children are a number? What doesn't work if this is just returning the children if there are any, otherwise the config.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This used to always return children for arrays/collections. But as part of my change, I might have to return the parent object or children object (when it contains array as a children vs non-array).
If children are a number, that means the children are an array and we select that. Otherwise, we select the parent object as a single-element array.
When there are nested objects (keys - "a:b", "a:c"), there are children but they cannot be bound. You have to select the parent object ("a" itself) for the single-element array to be bound.

{
children = config.GetChildren();
}
else
{
children = new[] { config as IConfigurationSection };
}

return children;
}

private static bool ShouldBindSingleElementsToArray()
{
if (AppContext.TryGetSwitch(BindSingleElementsToArraySwitch, out bool bindSingleElementsToArray))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to be cached? Reading from AppContext each time in a loop might be a perf issue.

{
return bindSingleElementsToArray;
}

// Enable this switch by default.
return true;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,34 @@ public void ExceptionWhenTryingToBindToByteArray()
exception.Message);
}

[Fact]
public void CanBindSingleElementToCollection()
{
var dic = new Dictionary<string, string>
{
{"MyString", "hello world"},
{"Nested:Integer", "11"},
};

var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddInMemoryCollection(dic);
var config = configurationBuilder.Build();

var stringArr = config.GetSection("MyString").Get<string[]>();
Assert.Equal("hello world", stringArr[0]);
Assert.Equal(1, stringArr.Length);

var stringAsStr = config.GetSection("MyString").Get<string>();
Assert.Equal("hello world", stringAsStr);

var nested = config.GetSection("Nested").Get<NestedOptions>();
Assert.Equal(11, nested.Integer);

var nestedAsArray = config.GetSection("Nested").Get<NestedOptions[]>();
Assert.Equal(11, nestedAsArray[0].Integer);
Assert.Equal(1, nestedAsArray.Length);
}

private interface ISomeInterface
{
}
Expand Down