Skip to content

Commit ce46be3

Browse files
authored
[release/7.0] Fix Configuration Binding with Instantiated Objects (#81250)
* Fix Configuration Binding with Instantiated Objects * Increment the servicing version
1 parent 13a9019 commit ce46be3

File tree

4 files changed

+240
-42
lines changed

4 files changed

+240
-42
lines changed

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

Lines changed: 58 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -315,12 +315,15 @@ private static void BindInstance(
315315
}
316316

317317
// for sets and read-only set interfaces, we clone what's there into a new collection, if we can
318-
if (TypeIsASetInterface(type) && !bindingPoint.IsReadOnly)
318+
if (TypeIsASetInterface(type))
319319
{
320-
object? newValue = BindSet(type, (IEnumerable?)bindingPoint.Value, config, options);
321-
if (newValue != null)
320+
if (!bindingPoint.IsReadOnly || bindingPoint.Value is not null)
322321
{
323-
bindingPoint.SetValue(newValue);
322+
object? newValue = BindSet(type, (IEnumerable?)bindingPoint.Value, config, options);
323+
if (!bindingPoint.IsReadOnly && newValue != null)
324+
{
325+
bindingPoint.SetValue(newValue);
326+
}
324327
}
325328

326329
return;
@@ -528,33 +531,41 @@ private static bool CanBindToTheseConstructorParameters(ParameterInfo[] construc
528531
return null;
529532
}
530533

531-
Type genericType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
532-
MethodInfo addMethod = genericType.GetMethod("Add", DeclaredOnlyLookup)!;
533-
534-
Type kvpType = typeof(KeyValuePair<,>).MakeGenericType(keyType, valueType);
535-
PropertyInfo keyMethod = kvpType.GetProperty("Key", DeclaredOnlyLookup)!;
536-
PropertyInfo valueMethod = kvpType.GetProperty("Value", DeclaredOnlyLookup)!;
537-
538-
object dictionary = Activator.CreateInstance(genericType)!;
539-
540-
var orig = source as IEnumerable;
541-
object?[] arguments = new object?[2];
542-
543-
if (orig != null)
534+
// addMethod can only be null if dictionaryType is IReadOnlyDictionary<TKey, TValue> rather than IDictionary<TKey, TValue>.
535+
MethodInfo? addMethod = dictionaryType.GetMethod("Add", DeclaredOnlyLookup);
536+
if (addMethod is null || source is null)
544537
{
545-
foreach (object? item in orig)
538+
dictionaryType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
539+
var dictionary = Activator.CreateInstance(dictionaryType);
540+
addMethod = dictionaryType.GetMethod("Add", DeclaredOnlyLookup);
541+
542+
var orig = source as IEnumerable;
543+
if (orig is not null)
546544
{
547-
object? k = keyMethod.GetMethod!.Invoke(item, null);
548-
object? v = valueMethod.GetMethod!.Invoke(item, null);
549-
arguments[0] = k;
550-
arguments[1] = v;
551-
addMethod.Invoke(dictionary, arguments);
545+
Type kvpType = typeof(KeyValuePair<,>).MakeGenericType(keyType, valueType);
546+
PropertyInfo keyMethod = kvpType.GetProperty("Key", DeclaredOnlyLookup)!;
547+
PropertyInfo valueMethod = kvpType.GetProperty("Value", DeclaredOnlyLookup)!;
548+
object?[] arguments = new object?[2];
549+
550+
foreach (object? item in orig)
551+
{
552+
object? k = keyMethod.GetMethod!.Invoke(item, null);
553+
object? v = valueMethod.GetMethod!.Invoke(item, null);
554+
arguments[0] = k;
555+
arguments[1] = v;
556+
addMethod!.Invoke(dictionary, arguments);
557+
}
552558
}
559+
560+
source = dictionary;
553561
}
554562

555-
BindDictionary(dictionary, genericType, config, options);
563+
Debug.Assert(source is not null);
564+
Debug.Assert(addMethod is not null);
565+
566+
BindDictionary(source, dictionaryType, config, options);
556567

557-
return dictionary;
568+
return source;
558569
}
559570

560571
// Binds and potentially overwrites a dictionary object.
@@ -727,32 +738,38 @@ private static Array BindArray(Type type, IEnumerable? source, IConfiguration co
727738
{
728739
Type elementType = type.GetGenericArguments()[0];
729740

730-
Type keyType = type.GenericTypeArguments[0];
741+
bool keyTypeIsEnum = elementType.IsEnum;
731742

732-
bool keyTypeIsEnum = keyType.IsEnum;
733-
734-
if (keyType != typeof(string) && !keyTypeIsEnum)
743+
if (elementType != typeof(string) && !keyTypeIsEnum)
735744
{
736745
// We only support string and enum keys
737746
return null;
738747
}
739748

740-
Type genericType = typeof(HashSet<>).MakeGenericType(keyType);
741-
object instance = Activator.CreateInstance(genericType)!;
742-
743-
MethodInfo addMethod = genericType.GetMethod("Add", DeclaredOnlyLookup)!;
744-
745749
object?[] arguments = new object?[1];
746-
747-
if (source != null)
750+
// addMethod can only be null if type is IReadOnlySet<T> rather than ISet<T>.
751+
MethodInfo? addMethod = type.GetMethod("Add", DeclaredOnlyLookup);
752+
if (addMethod is null || source is null)
748753
{
749-
foreach (object? item in source)
754+
Type genericType = typeof(HashSet<>).MakeGenericType(elementType);
755+
object instance = Activator.CreateInstance(genericType)!;
756+
addMethod = genericType.GetMethod("Add", DeclaredOnlyLookup);
757+
758+
if (source != null)
750759
{
751-
arguments[0] = item;
752-
addMethod.Invoke(instance, arguments);
760+
foreach (object? item in source)
761+
{
762+
arguments[0] = item;
763+
addMethod!.Invoke(instance, arguments);
764+
}
753765
}
766+
767+
source = (IEnumerable)instance;
754768
}
755769

770+
Debug.Assert(source is not null);
771+
Debug.Assert(addMethod is not null);
772+
756773
foreach (IConfigurationSection section in config.GetChildren())
757774
{
758775
var itemBindingPoint = new BindingPoint();
@@ -767,15 +784,15 @@ private static Array BindArray(Type type, IEnumerable? source, IConfiguration co
767784
{
768785
arguments[0] = itemBindingPoint.Value;
769786

770-
addMethod.Invoke(instance, arguments);
787+
addMethod.Invoke(source, arguments);
771788
}
772789
}
773790
catch
774791
{
775792
}
776793
}
777794

778-
return instance;
795+
return source;
779796
}
780797

781798
[RequiresUnreferencedCode(TrimmingWarningMessage)]

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<EnableDefaultItems>true</EnableDefaultItems>
66
<IsPackable>true</IsPackable>
77
<EnableAOTAnalyzer>true</EnableAOTAnalyzer>
8-
<ServicingVersion>3</ServicingVersion>
8+
<ServicingVersion>4</ServicingVersion>
99
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
1010
<PackageDescription>Functionality to bind an object to data in configuration providers for Microsoft.Extensions.Configuration.</PackageDescription>
1111
</PropertyGroup>

src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1685,5 +1685,182 @@ public class ExtendedDictionary<TKey, TValue> : Dictionary<TKey, TValue>
16851685
{
16861686

16871687
}
1688+
1689+
private class OptionsWithDifferentCollectionInterfaces
1690+
{
1691+
private static IEnumerable<string> s_instantiatedIEnumerable = new List<string> { "value1", "value2" };
1692+
public bool IsSameInstantiatedIEnumerable() => object.ReferenceEquals(s_instantiatedIEnumerable, InstantiatedIEnumerable);
1693+
public IEnumerable<string> InstantiatedIEnumerable { get; set; } = s_instantiatedIEnumerable;
1694+
1695+
private static IList<string> s_instantiatedIList = new List<string> { "value1", "value2" };
1696+
public bool IsSameInstantiatedIList() => object.ReferenceEquals(s_instantiatedIList, InstantiatedIList);
1697+
public IList<string> InstantiatedIList { get; set; } = s_instantiatedIList;
1698+
1699+
private static IReadOnlyList<string> s_instantiatedIReadOnlyList = new List<string> { "value1", "value2" };
1700+
public bool IsSameInstantiatedIReadOnlyList() => object.ReferenceEquals(s_instantiatedIReadOnlyList, InstantiatedIReadOnlyList);
1701+
public IReadOnlyList<string> InstantiatedIReadOnlyList { get; set; } = s_instantiatedIReadOnlyList;
1702+
1703+
private static IDictionary<string, string> s_instantiatedIDictionary = new Dictionary<string, string> { ["Key1"] = "value1", ["Key2"] = "value2" };
1704+
public IDictionary<string, string> InstantiatedIDictionary { get; set; } = s_instantiatedIDictionary;
1705+
public bool IsSameInstantiatedIDictionary() => object.ReferenceEquals(s_instantiatedIDictionary, InstantiatedIDictionary);
1706+
1707+
private static IReadOnlyDictionary<string, string> s_instantiatedIReadOnlyDictionary = new Dictionary<string, string> { ["Key1"] = "value1", ["Key2"] = "value2" };
1708+
public IReadOnlyDictionary<string, string> InstantiatedIReadOnlyDictionary { get; set; } = s_instantiatedIReadOnlyDictionary;
1709+
public bool IsSameInstantiatedIReadOnlyDictionary() => object.ReferenceEquals(s_instantiatedIReadOnlyDictionary, InstantiatedIReadOnlyDictionary);
1710+
1711+
private static ISet<string> s_instantiatedISet = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "a", "A", "b" };
1712+
public ISet<string> InstantiatedISet { get; set; } = s_instantiatedISet;
1713+
public bool IsSameInstantiatedISet() => object.ReferenceEquals(s_instantiatedISet, InstantiatedISet);
1714+
1715+
#if NETCOREAPP
1716+
private static IReadOnlySet<string> s_instantiatedIReadOnlySet = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "a", "A", "b" };
1717+
public IReadOnlySet<string> InstantiatedIReadOnlySet { get; set; } = s_instantiatedIReadOnlySet;
1718+
public bool IsSameInstantiatedIReadOnlySet() => object.ReferenceEquals(s_instantiatedIReadOnlySet, InstantiatedIReadOnlySet);
1719+
1720+
public IReadOnlySet<string> UnInstantiatedIReadOnlySet { get; set; }
1721+
#endif
1722+
private static ICollection<string> s_instantiatedICollection = new List<string> { "a", "b", "c" };
1723+
public ICollection<string> InstantiatedICollection { get; set; } = s_instantiatedICollection;
1724+
public bool IsSameInstantiatedICollection() => object.ReferenceEquals(s_instantiatedICollection, InstantiatedICollection);
1725+
1726+
private static IReadOnlyCollection<string> s_instantiatedIReadOnlyCollection = new List<string> { "a", "b", "c" };
1727+
public IReadOnlyCollection<string> InstantiatedIReadOnlyCollection { get; set; } = s_instantiatedIReadOnlyCollection;
1728+
public bool IsSameInstantiatedIReadOnlyCollection() => object.ReferenceEquals(s_instantiatedIReadOnlyCollection, InstantiatedIReadOnlyCollection);
1729+
1730+
public IReadOnlyCollection<string> UnInstantiatedIReadOnlyCollection { get; set; }
1731+
public ICollection<string> UnInstantiatedICollection { get; set; }
1732+
public ISet<string> UnInstantiatedISet { get; set; }
1733+
public IReadOnlyDictionary<string, string> UnInstantiatedIReadOnlyDictionary { get; set; }
1734+
public IEnumerable<string> UnInstantiatedIEnumerable { get; set; }
1735+
public IList<string> UnInstantiatedIList { get; set; }
1736+
public IReadOnlyList<string> UnInstantiatedIReadOnlyList { get; set; }
1737+
}
1738+
[Fact]
1739+
public void TestOptionsWithDifferentCollectionInterfaces()
1740+
{
1741+
var input = new Dictionary<string, string>
1742+
{
1743+
{"InstantiatedIEnumerable:0", "value3"},
1744+
{"UnInstantiatedIEnumerable:0", "value1"},
1745+
{"InstantiatedIList:0", "value3"},
1746+
{"InstantiatedIReadOnlyList:0", "value3"},
1747+
{"UnInstantiatedIReadOnlyList:0", "value"},
1748+
{"UnInstantiatedIList:0", "value"},
1749+
{"InstantiatedIDictionary:Key3", "value3"},
1750+
{"InstantiatedIReadOnlyDictionary:Key3", "value3"},
1751+
{"UnInstantiatedIReadOnlyDictionary:Key", "value"},
1752+
{"InstantiatedISet:0", "B"},
1753+
{"InstantiatedISet:1", "C"},
1754+
{"UnInstantiatedISet:0", "a"},
1755+
{"UnInstantiatedISet:1", "A"},
1756+
{"UnInstantiatedISet:2", "B"},
1757+
{"InstantiatedIReadOnlySet:0", "Z"},
1758+
{"UnInstantiatedIReadOnlySet:0", "y"},
1759+
{"UnInstantiatedIReadOnlySet:1", "z"},
1760+
{"InstantiatedICollection:0", "d"},
1761+
{"UnInstantiatedICollection:0", "t"},
1762+
{"UnInstantiatedICollection:1", "a"},
1763+
{"InstantiatedIReadOnlyCollection:0", "d"},
1764+
{"UnInstantiatedIReadOnlyCollection:0", "r"},
1765+
{"UnInstantiatedIReadOnlyCollection:1", "e"},
1766+
};
1767+
1768+
var configurationBuilder = new ConfigurationBuilder();
1769+
configurationBuilder.AddInMemoryCollection(input);
1770+
var config = configurationBuilder.Build();
1771+
1772+
var options = new OptionsWithDifferentCollectionInterfaces();
1773+
config.Bind(options);
1774+
1775+
Assert.True(3 == options.InstantiatedIEnumerable.Count(), $"InstantiatedIEnumerable count is {options.InstantiatedIEnumerable.Count()} .. {options.InstantiatedIEnumerable.ElementAt(options.InstantiatedIEnumerable.Count() - 1)}");
1776+
Assert.Equal("value1", options.InstantiatedIEnumerable.ElementAt(0));
1777+
Assert.Equal("value2", options.InstantiatedIEnumerable.ElementAt(1));
1778+
Assert.Equal("value3", options.InstantiatedIEnumerable.ElementAt(2));
1779+
Assert.False(options.IsSameInstantiatedIEnumerable());
1780+
1781+
Assert.Equal(1, options.UnInstantiatedIEnumerable.Count());
1782+
Assert.Equal("value1", options.UnInstantiatedIEnumerable.ElementAt(0));
1783+
1784+
Assert.True(3 == options.InstantiatedIList.Count(), $"InstantiatedIList count is {options.InstantiatedIList.Count()} .. {options.InstantiatedIList[options.InstantiatedIList.Count() - 1]}");
1785+
Assert.Equal("value1", options.InstantiatedIList[0]);
1786+
Assert.Equal("value2", options.InstantiatedIList[1]);
1787+
Assert.Equal("value3", options.InstantiatedIList[2]);
1788+
Assert.True(options.IsSameInstantiatedIList());
1789+
1790+
Assert.Equal(1, options.UnInstantiatedIList.Count());
1791+
Assert.Equal("value", options.UnInstantiatedIList[0]);
1792+
1793+
Assert.True(3 == options.InstantiatedIReadOnlyList.Count(), $"InstantiatedIReadOnlyList count is {options.InstantiatedIReadOnlyList.Count()} .. {options.InstantiatedIReadOnlyList[options.InstantiatedIReadOnlyList.Count() - 1]}");
1794+
Assert.Equal("value1", options.InstantiatedIReadOnlyList[0]);
1795+
Assert.Equal("value2", options.InstantiatedIReadOnlyList[1]);
1796+
Assert.Equal("value3", options.InstantiatedIReadOnlyList[2]);
1797+
Assert.False(options.IsSameInstantiatedIReadOnlyList());
1798+
1799+
Assert.Equal(1, options.UnInstantiatedIReadOnlyList.Count());
1800+
Assert.Equal("value", options.UnInstantiatedIReadOnlyList[0]);
1801+
1802+
Assert.True(3 == options.InstantiatedIReadOnlyList.Count(), $"InstantiatedIReadOnlyList count is {options.InstantiatedIReadOnlyList.Count()} .. {options.InstantiatedIReadOnlyList[options.InstantiatedIReadOnlyList.Count() - 1]}");
1803+
Assert.Equal(new string[] { "Key1", "Key2", "Key3" }, options.InstantiatedIDictionary.Keys);
1804+
Assert.Equal(new string[] { "value1", "value2", "value3" }, options.InstantiatedIDictionary.Values);
1805+
Assert.True(options.IsSameInstantiatedIDictionary());
1806+
1807+
Assert.True(3 == options.InstantiatedIReadOnlyDictionary.Count(), $"InstantiatedIReadOnlyDictionary count is {options.InstantiatedIReadOnlyDictionary.Count()} .. {options.InstantiatedIReadOnlyDictionary.ElementAt(options.InstantiatedIReadOnlyDictionary.Count() - 1)}");
1808+
Assert.Equal(new string[] { "Key1", "Key2", "Key3" }, options.InstantiatedIReadOnlyDictionary.Keys);
1809+
Assert.Equal(new string[] { "value1", "value2", "value3" }, options.InstantiatedIReadOnlyDictionary.Values);
1810+
Assert.False(options.IsSameInstantiatedIReadOnlyDictionary());
1811+
1812+
Assert.Equal(1, options.UnInstantiatedIReadOnlyDictionary.Count());
1813+
Assert.Equal(new string[] { "Key" }, options.UnInstantiatedIReadOnlyDictionary.Keys);
1814+
Assert.Equal(new string[] { "value" }, options.UnInstantiatedIReadOnlyDictionary.Values);
1815+
1816+
Assert.True(3 == options.InstantiatedISet.Count(), $"InstantiatedISet count is {options.InstantiatedISet.Count()} .. {string.Join(", ", options.InstantiatedISet)} .. {options.IsSameInstantiatedISet()}");
1817+
Assert.Equal(new string[] { "a", "b", "C" }, options.InstantiatedISet);
1818+
Assert.True(options.IsSameInstantiatedISet());
1819+
1820+
Assert.True(3 == options.UnInstantiatedISet.Count(), $"UnInstantiatedISet count is {options.UnInstantiatedISet.Count()} .. {options.UnInstantiatedISet.ElementAt(options.UnInstantiatedISet.Count() - 1)}");
1821+
Assert.Equal(new string[] { "a", "A", "B" }, options.UnInstantiatedISet);
1822+
1823+
#if NETCOREAPP
1824+
Assert.True(3 == options.InstantiatedIReadOnlySet.Count(), $"InstantiatedIReadOnlySet count is {options.InstantiatedIReadOnlySet.Count()} .. {options.InstantiatedIReadOnlySet.ElementAt(options.InstantiatedIReadOnlySet.Count() - 1)}");
1825+
Assert.Equal(new string[] { "a", "b", "Z" }, options.InstantiatedIReadOnlySet);
1826+
Assert.False(options.IsSameInstantiatedIReadOnlySet());
1827+
1828+
Assert.Equal(2, options.UnInstantiatedIReadOnlySet.Count());
1829+
Assert.Equal(new string[] { "y", "z" }, options.UnInstantiatedIReadOnlySet);
1830+
#endif
1831+
Assert.Equal(4, options.InstantiatedICollection.Count());
1832+
Assert.Equal(new string[] { "a", "b", "c", "d" }, options.InstantiatedICollection);
1833+
Assert.True(options.IsSameInstantiatedICollection());
1834+
1835+
Assert.Equal(2, options.UnInstantiatedICollection.Count());
1836+
Assert.Equal(new string[] { "t", "a" }, options.UnInstantiatedICollection);
1837+
1838+
Assert.Equal(4, options.InstantiatedIReadOnlyCollection.Count());
1839+
Assert.Equal(new string[] { "a", "b", "c", "d" }, options.InstantiatedIReadOnlyCollection);
1840+
Assert.False(options.IsSameInstantiatedIReadOnlyCollection());
1841+
1842+
Assert.Equal(2, options.UnInstantiatedIReadOnlyCollection.Count());
1843+
Assert.Equal(new string[] { "r", "e" }, options.UnInstantiatedIReadOnlyCollection);
1844+
}
1845+
1846+
[Fact]
1847+
public void TestMutatingDictionaryValues()
1848+
{
1849+
IConfiguration config = new ConfigurationBuilder()
1850+
.AddInMemoryCollection()
1851+
.Build();
1852+
1853+
config["Key:0"] = "NewValue";
1854+
var dict = new Dictionary<string, string[]>() { { "Key", new[] { "InitialValue" } } };
1855+
1856+
Assert.Equal(1, dict["Key"].Length);
1857+
Assert.Equal("InitialValue", dict["Key"][0]);
1858+
1859+
// Binding will accumulate to the values inside the dictionary.
1860+
config.Bind(dict);
1861+
Assert.Equal(2, dict["Key"].Length);
1862+
Assert.Equal("InitialValue", dict["Key"][0]);
1863+
Assert.Equal("NewValue", dict["Key"][1]);
1864+
}
16881865
}
16891866
}

src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ILLink.Descriptors.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@
1515
<method signature="System.Void .ctor()" />
1616
</type>
1717
</assembly>
18+
19+
<assembly fullname="System.Private.Corelib">
20+
<type fullname="System.Collections.Generic.ISet`1" preserve="methods" />
21+
</assembly>
1822
</linker>

0 commit comments

Comments
 (0)