Skip to content

Commit

Permalink
Fixes Binding to non-null IEnumerable doesn't work #36390 (#66131)
Browse files Browse the repository at this point in the history
  • Loading branch information
SteveDunn authored Mar 17, 2022
1 parent c032e0d commit 048da75
Show file tree
Hide file tree
Showing 3 changed files with 299 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
Expand Down Expand Up @@ -378,10 +380,19 @@ private static void BindProperty(PropertyInfo property, object instance, IConfig
{
BindCollection(instance, collectionInterface, config, options);
}
// Something else
else
{
BindNonScalar(config, instance, options);
// See if its an IEnumerable
collectionInterface = FindOpenGenericInterface(typeof(IEnumerable<>), type);
if (collectionInterface != null)
{
instance = BindExistingCollection((IEnumerable)instance!, config, options);
}
// Something else
else
{
BindNonScalar(config, instance, options);
}
}
}
}
Expand Down Expand Up @@ -498,6 +509,41 @@ private static void BindCollection(
}
}

[RequiresUnreferencedCode("Cannot statically analyze what the element type is of the object collection so its members may be trimmed.")]
private static IEnumerable BindExistingCollection(IEnumerable source, IConfiguration config, BinderOptions options)
{
// find the interface that is IEnumerable<T>
Type type = source.GetType().GetInterface("IEnumerable`1", false)!;
Type elementType = type.GenericTypeArguments[0];
Type genericType = typeof(List<>).MakeGenericType(elementType);

IList newList = (IList)Activator.CreateInstance(genericType, source)!;

IConfigurationSection[] children = config.GetChildren().ToArray();

for (int i = 0; i < children.Length; i++)
{
try
{
object? item = BindInstance(
type: elementType,
instance: null,
config: children[i],
options: options);

if (item != null)
{
newList.Add(item);
}
}
catch
{
}
}

return newList;
}

[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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using Xunit;

Expand Down Expand Up @@ -47,6 +48,11 @@ public string ReadOnly
{
get { return null; }
}

public IEnumerable<string> NonInstantiatedIEnumerable { get; set; } = null!;
public IEnumerable<string> InstantiatedIEnumerable { get; set; } = new List<string>();
public ICollection<string> InstantiatedICollection { get; set; } = new List<string>();
public IReadOnlyCollection<string> InstantiatedIReadOnlyCollection { get; set; } = new List<string>();
}

public class NestedOptions
Expand Down Expand Up @@ -220,6 +226,107 @@ public void CanBindConfigurationKeyNameAttributes()
Assert.Equal("Yo", options.NamedProperty);
}

[Fact]
public void CanBindNonInstantiatedIEnumerableWithItems()
{
var dic = new Dictionary<string, string>
{
{"NonInstantiatedIEnumerable:0", "Yo1"},
{"NonInstantiatedIEnumerable:1", "Yo2"},
};
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddInMemoryCollection(dic);

var config = configurationBuilder.Build();

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

Assert.Equal(2, options.NonInstantiatedIEnumerable.Count());
Assert.Equal("Yo1", options.NonInstantiatedIEnumerable.ElementAt(0));
Assert.Equal("Yo2", options.NonInstantiatedIEnumerable.ElementAt(1));
}

[Fact]
public void CanBindInstantiatedIEnumerableWithItems()
{
var dic = new Dictionary<string, string>
{
{"InstantiatedIEnumerable:0", "Yo1"},
{"InstantiatedIEnumerable:1", "Yo2"},
};
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddInMemoryCollection(dic);

var config = configurationBuilder.Build();

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

Assert.Equal(2, options.InstantiatedIEnumerable.Count());
Assert.Equal("Yo1", options.InstantiatedIEnumerable.ElementAt(0));
Assert.Equal("Yo2", options.InstantiatedIEnumerable.ElementAt(1));
}

[Fact]
public void CanBindInstantiatedICollectionWithItems()
{
var dic = new Dictionary<string, string>
{
{"InstantiatedICollection:0", "Yo1"},
{"InstantiatedICollection:1", "Yo2"},
};
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddInMemoryCollection(dic);

var config = configurationBuilder.Build();

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

Assert.Equal(2, options.InstantiatedICollection.Count());
Assert.Equal("Yo1", options.InstantiatedICollection.ElementAt(0));
Assert.Equal("Yo2", options.InstantiatedICollection.ElementAt(1));
}

[Fact]
public void CanBindInstantiatedIReadOnlyCollectionWithItems()
{
var dic = new Dictionary<string, string>
{
{"InstantiatedIReadOnlyCollection:0", "Yo1"},
{"InstantiatedIReadOnlyCollection:1", "Yo2"},
};
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddInMemoryCollection(dic);

var config = configurationBuilder.Build();

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

Assert.Equal(2, options.InstantiatedIReadOnlyCollection.Count);
Assert.Equal("Yo1", options.InstantiatedIReadOnlyCollection.ElementAt(0));
Assert.Equal("Yo2", options.InstantiatedIReadOnlyCollection.ElementAt(1));
}

[Fact]
public void CanBindInstantiatedIEnumerableWithNullItems()
{
var dic = new Dictionary<string, string>
{
{"InstantiatedIEnumerable:0", null},
{"InstantiatedIEnumerable:1", "Yo1"},
{"InstantiatedIEnumerable:2", "Yo2"},
};
var configurationBuilder = new ConfigurationBuilder();
configurationBuilder.AddInMemoryCollection(dic);

var config = configurationBuilder.Build();

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

Assert.Equal(2, options.InstantiatedIEnumerable.Count());
Assert.Equal("Yo1", options.InstantiatedIEnumerable.ElementAt(0));
Assert.Equal("Yo2", options.InstantiatedIEnumerable.ElementAt(1));
}

[Fact]
public void EmptyStringIsNullable()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Xunit;
Expand Down Expand Up @@ -958,6 +959,101 @@ public void CanBindUninitializedIEnumerable()
Assert.Equal("valx", array[3]);
}

[Fact]
public void CanBindInitializedIEnumerableAndTheOriginalItemsAreNotMutated()
{
var input = new Dictionary<string, string>
{
{"AlreadyInitializedIEnumerableInterface:0", "val0"},
{"AlreadyInitializedIEnumerableInterface:1", "val1"},
{"AlreadyInitializedIEnumerableInterface:2", "val2"},
{"AlreadyInitializedIEnumerableInterface:x", "valx"}
};

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

var options = new InitializedCollectionsOptions();
config.Bind(options);

var array = options.AlreadyInitializedIEnumerableInterface.ToArray();

Assert.Equal(6, array.Length);

Assert.Equal("This was here too", array[0]);
Assert.Equal("Don't touch me!", array[1]);
Assert.Equal("val0", array[2]);
Assert.Equal("val1", array[3]);
Assert.Equal("val2", array[4]);
Assert.Equal("valx", array[5]);

// the original list hasn't been touched
Assert.Equal(2, options.ListUsedInIEnumerableFieldAndShouldNotBeTouched.Count);
Assert.Equal("This was here too", options.ListUsedInIEnumerableFieldAndShouldNotBeTouched.ElementAt(0));
Assert.Equal("Don't touch me!", options.ListUsedInIEnumerableFieldAndShouldNotBeTouched.ElementAt(1));
}

[Fact]
public void CanBindInitializedCustomIEnumerableBasedList()
{
// A field declared as IEnumerable<T> that is instantiated with a class
// that directly implements IEnumerable<T> is still bound, but with
// a new List<T> with the original values copied over.

var input = new Dictionary<string, string>
{
{"AlreadyInitializedCustomListDerivedFromIEnumerable:0", "val0"},
{"AlreadyInitializedCustomListDerivedFromIEnumerable:1", "val1"},
};

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

var options = new InitializedCollectionsOptions();
config.Bind(options);

var array = options.AlreadyInitializedCustomListDerivedFromIEnumerable.ToArray();

Assert.Equal(4, array.Length);

Assert.Equal("Item1", array[0]);
Assert.Equal("Item2", array[1]);
Assert.Equal("val0", array[2]);
Assert.Equal("val1", array[3]);
}

[Fact]
public void CanBindInitializedCustomIndirectlyDerivedIEnumerableList()
{
// A field declared as IEnumerable<T> that is instantiated with a class
// that indirectly implements IEnumerable<T> is still bound, but with
// a new List<T> with the original values copied over.

var input = new Dictionary<string, string>
{
{"AlreadyInitializedCustomListIndirectlyDerivedFromIEnumerable:0", "val0"},
{"AlreadyInitializedCustomListIndirectlyDerivedFromIEnumerable:1", "val1"},
};

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

var options = new InitializedCollectionsOptions();
config.Bind(options);

var array = options.AlreadyInitializedCustomListIndirectlyDerivedFromIEnumerable.ToArray();

Assert.Equal(4, array.Length);

Assert.Equal("Item1", array[0]);
Assert.Equal("Item2", array[1]);
Assert.Equal("val0", array[2]);
Assert.Equal("val1", array[3]);
}

[Fact]
public void CanBindUninitializedICollection()
{
Expand Down Expand Up @@ -1101,6 +1197,28 @@ private class UnintializedCollectionsOptions
public IReadOnlyDictionary<string, string> IReadOnlyDictionary { get; set; }
}

private class InitializedCollectionsOptions
{
public InitializedCollectionsOptions()
{
AlreadyInitializedIEnumerableInterface = ListUsedInIEnumerableFieldAndShouldNotBeTouched;
}

public List<string> ListUsedInIEnumerableFieldAndShouldNotBeTouched = new List<string>
{
"This was here too",
"Don't touch me!"
};

public IEnumerable<string> AlreadyInitializedIEnumerableInterface { get; set; }

public IEnumerable<string> AlreadyInitializedCustomListDerivedFromIEnumerable { get; set; } =
new CustomListDerivedFromIEnumerable();

public IEnumerable<string> AlreadyInitializedCustomListIndirectlyDerivedFromIEnumerable { get; set; } =
new CustomListIndirectlyDerivedFromIEnumerable();
}

private class CustomList : List<string>
{
// Add an overload, just to make sure binding picks the right Add method
Expand All @@ -1109,6 +1227,32 @@ public void Add(string a, string b)
}
}

private class CustomListDerivedFromIEnumerable : IEnumerable<string>
{
private readonly List<string> _items = new List<string> { "Item1", "Item2" };

public IEnumerator<string> GetEnumerator() => _items.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

internal interface IDerivedOne : IDerivedTwo
{
}

internal interface IDerivedTwo : IEnumerable<string>
{
}

private class CustomListIndirectlyDerivedFromIEnumerable : IDerivedOne
{
private readonly List<string> _items = new List<string> { "Item1", "Item2" };

public IEnumerator<string> GetEnumerator() => _items.GetEnumerator();

IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

private class CustomDictionary<T> : Dictionary<string, T>
{
}
Expand Down

0 comments on commit 048da75

Please sign in to comment.