Skip to content

Commit 091801a

Browse files
[release/7.0] Fix Binding with IList<>, ICollection and IDictionary<,> implementer types (#78118)
* Fix Binding with IDictionary<,> implementer types * Address the feedback * Feedback addressing * Fix Configuration with IList and ICollection (#77857) * Add servicing version to the source project Co-authored-by: Tarek Mahmoud Sayed <[email protected]>
1 parent e473891 commit 091801a

File tree

3 files changed

+110
-31
lines changed

3 files changed

+110
-31
lines changed

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

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -299,20 +299,16 @@ private static void BindInstance(
299299

300300
if (config != null && config.GetChildren().Any())
301301
{
302-
// for arrays, collections, and read-only list-like interfaces, we concatenate on to what is already there, if we can
303-
if (type.IsArray || IsArrayCompatibleInterface(type))
302+
// for arrays and read-only list-like interfaces, we concatenate on to what is already there, if we can
303+
if (type.IsArray || IsImmutableArrayCompatibleInterface(type))
304304
{
305305
if (!bindingPoint.IsReadOnly)
306306
{
307307
bindingPoint.SetValue(BindArray(type, (IEnumerable?)bindingPoint.Value, config, options));
308-
return;
309308
}
310309

311310
// for getter-only collection properties that we can't add to, nothing more we can do
312-
if (type.IsArray || IsImmutableArrayCompatibleInterface(type))
313-
{
314-
return;
315-
}
311+
return;
316312
}
317313

318314
// for sets and read-only set interfaces, we clone what's there into a new collection, if we can
@@ -350,12 +346,19 @@ private static void BindInstance(
350346
return;
351347
}
352348

353-
// For other mutable interfaces like ICollection<> and ISet<>, we prefer copying values and setting them
354-
// on a new instance of the interface over populating the existing instance implementing the interface.
355-
// This has already been done, so there's not need to check again. For dictionaries, we fill the existing
356-
// instance if there is one (which hasn't happened yet), and only create a new instance if necessary.
349+
Type? interfaceGenericType = type.IsInterface && type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : null;
357350

358-
bindingPoint.SetValue(CreateInstance(type, config, options));
351+
if (interfaceGenericType is not null &&
352+
(interfaceGenericType == typeof(ICollection<>) || interfaceGenericType == typeof(IList<>)))
353+
{
354+
// For ICollection<T> and IList<T> we bind them to mutable List<T> type.
355+
Type genericType = typeof(List<>).MakeGenericType(type.GenericTypeArguments[0]);
356+
bindingPoint.SetValue(Activator.CreateInstance(genericType));
357+
}
358+
else
359+
{
360+
bindingPoint.SetValue(CreateInstance(type, config, options));
361+
}
359362
}
360363

361364
// At this point we know that we have a non-null bindingPoint.Value, we just have to populate the items
@@ -554,9 +557,9 @@ private static bool CanBindToTheseConstructorParameters(ParameterInfo[] construc
554557
// Binds and potentially overwrites a concrete dictionary.
555558
// This differs from BindDictionaryInterface because this method doesn't clone
556559
// the dictionary; it sets and/or overwrites values directly.
557-
// When a user specifies a concrete dictionary in their config class, then that
558-
// value is used as-us. When a user specifies an interface (instantiated) in their config class,
559-
// then it is cloned to a new dictionary, the same way as other collections.
560+
// When a user specifies a concrete dictionary or a concrete class implementing IDictionary<,>
561+
// in their config class, then that value is used as-is. When a user specifies an interface (instantiated)
562+
// in their config class, then it is cloned to a new dictionary, the same way as other collections.
560563
[RequiresDynamicCode(DynamicCodeWarningMessage)]
561564
[RequiresUnreferencedCode("Cannot statically analyze what the element type is of the value objects in the dictionary so its members may be trimmed.")]
562565
private static void BindConcreteDictionary(
@@ -584,10 +587,20 @@ private static void BindConcreteDictionary(
584587
return;
585588
}
586589

587-
Type genericType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
590+
Debug.Assert(dictionary is not null);
591+
592+
Type dictionaryObjectType = dictionary.GetType();
593+
594+
MethodInfo tryGetValue = dictionaryObjectType.GetMethod("TryGetValue", BindingFlags.Public | BindingFlags.Instance)!;
595+
596+
// dictionary should be of type Dictionary<,> or of type implementing IDictionary<,>
597+
PropertyInfo? setter = dictionaryObjectType.GetProperty("Item", BindingFlags.Public | BindingFlags.Instance);
598+
if (setter is null || !setter.CanWrite)
599+
{
600+
// Cannot set any item on the dictionary object.
601+
return;
602+
}
588603

589-
MethodInfo tryGetValue = dictionaryType.GetMethod("TryGetValue")!;
590-
PropertyInfo setter = genericType.GetProperty("Item", DeclaredOnlyLookup)!;
591604
foreach (IConfigurationSection child in config.GetChildren())
592605
{
593606
try
@@ -838,18 +851,6 @@ private static bool TypeIsADictionaryInterface(Type type)
838851
|| genericTypeDefinition == typeof(IReadOnlyDictionary<,>);
839852
}
840853

841-
private static bool IsArrayCompatibleInterface(Type type)
842-
{
843-
if (!type.IsInterface || !type.IsConstructedGenericType) { return false; }
844-
845-
Type genericTypeDefinition = type.GetGenericTypeDefinition();
846-
return genericTypeDefinition == typeof(IEnumerable<>)
847-
|| genericTypeDefinition == typeof(ICollection<>)
848-
|| genericTypeDefinition == typeof(IList<>)
849-
|| genericTypeDefinition == typeof(IReadOnlyCollection<>)
850-
|| genericTypeDefinition == typeof(IReadOnlyList<>);
851-
}
852-
853854
private static bool IsImmutableArrayCompatibleInterface(Type type)
854855
{
855856
if (!type.IsInterface || !type.IsConstructedGenericType) { return false; }

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
<EnableDefaultItems>true</EnableDefaultItems>
66
<IsPackable>true</IsPackable>
77
<EnableAOTAnalyzer>true</EnableAOTAnalyzer>
8+
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
9+
<ServicingVersion>1</ServicingVersion>
810
<PackageDescription>Functionality to bind an object to data in configuration providers for Microsoft.Extensions.Configuration.</PackageDescription>
911
</PropertyGroup>
1012

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

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,11 @@ public void AlreadyInitializedListInterfaceBinding()
393393
Assert.Equal("val1", list[2]);
394394
Assert.Equal("val2", list[3]);
395395
Assert.Equal("valx", list[4]);
396+
397+
// Ensure expandability of the returned list
398+
options.AlreadyInitializedListInterface.Add("ExtraItem");
399+
Assert.Equal(6, options.AlreadyInitializedListInterface.Count);
400+
Assert.Equal("ExtraItem", options.AlreadyInitializedListInterface[5]);
396401
}
397402

398403
[Fact]
@@ -1067,7 +1072,7 @@ public void CanBindInitializedIEnumerableAndTheOriginalItemsAreNotMutated()
10671072
{"AlreadyInitializedIEnumerableInterface:1", "val1"},
10681073
{"AlreadyInitializedIEnumerableInterface:2", "val2"},
10691074
{"AlreadyInitializedIEnumerableInterface:x", "valx"},
1070-
1075+
10711076
{"ICollectionNoSetter:0", "val0"},
10721077
{"ICollectionNoSetter:1", "val1"},
10731078
};
@@ -1098,6 +1103,11 @@ public void CanBindInitializedIEnumerableAndTheOriginalItemsAreNotMutated()
10981103
Assert.Equal(2, options.ICollectionNoSetter.Count);
10991104
Assert.Equal("val0", options.ICollectionNoSetter.ElementAt(0));
11001105
Assert.Equal("val1", options.ICollectionNoSetter.ElementAt(1));
1106+
1107+
// Ensure expandability of the returned collection
1108+
options.ICollectionNoSetter.Add("ExtraItem");
1109+
Assert.Equal(3, options.ICollectionNoSetter.Count);
1110+
Assert.Equal("ExtraItem", options.ICollectionNoSetter.ElementAt(2));
11011111
}
11021112

11031113
[Fact]
@@ -1218,6 +1228,11 @@ public void CanBindUninitializedICollection()
12181228
Assert.Equal("val1", array[1]);
12191229
Assert.Equal("val2", array[2]);
12201230
Assert.Equal("valx", array[3]);
1231+
1232+
// Ensure expandability of the returned collection
1233+
options.ICollection.Add("ExtraItem");
1234+
Assert.Equal(5, options.ICollection.Count);
1235+
Assert.Equal("ExtraItem", options.ICollection.ElementAt(4));
12211236
}
12221237

12231238
[Fact]
@@ -1246,6 +1261,11 @@ public void CanBindUninitializedIList()
12461261
Assert.Equal("val1", list[1]);
12471262
Assert.Equal("val2", list[2]);
12481263
Assert.Equal("valx", list[3]);
1264+
1265+
// Ensure expandability of the returned list
1266+
options.IList.Add("ExtraItem");
1267+
Assert.Equal(5, options.IList.Count);
1268+
Assert.Equal("ExtraItem", options.IList[4]);
12491269
}
12501270

12511271
[Fact]
@@ -1602,5 +1622,61 @@ private class OptionsWithInterdependentProperties
16021622
public IEnumerable<int> FilteredConfigValues => ConfigValues.Where(p => p > 10);
16031623
public IEnumerable<int> ConfigValues { get; set; }
16041624
}
1625+
1626+
[Fact]
1627+
public void DifferentDictionaryBindingCasesTest()
1628+
{
1629+
var dic = new Dictionary<string, string>() { { "key", "value" } };
1630+
var config = new ConfigurationBuilder()
1631+
.AddInMemoryCollection(dic)
1632+
.Build();
1633+
1634+
Assert.Single(config.Get<Dictionary<string, string>>());
1635+
Assert.Single(config.Get<IDictionary<string, string>>());
1636+
Assert.Single(config.Get<ExtendedDictionary<string, string>>());
1637+
Assert.Single(config.Get<ImplementerOfIDictionaryClass<string, string>>());
1638+
}
1639+
1640+
public class ImplementerOfIDictionaryClass<TKey, TValue> : IDictionary<TKey, TValue>
1641+
{
1642+
private Dictionary<TKey, TValue> _dict = new();
1643+
1644+
public TValue this[TKey key] { get => _dict[key]; set => _dict[key] = value; }
1645+
1646+
public ICollection<TKey> Keys => _dict.Keys;
1647+
1648+
public ICollection<TValue> Values => _dict.Values;
1649+
1650+
public int Count => _dict.Count;
1651+
1652+
public bool IsReadOnly => false;
1653+
1654+
public void Add(TKey key, TValue value) => _dict.Add(key, value);
1655+
1656+
public void Add(KeyValuePair<TKey, TValue> item) => _dict.Add(item.Key, item.Value);
1657+
1658+
public void Clear() => _dict.Clear();
1659+
1660+
public bool Contains(KeyValuePair<TKey, TValue> item) => _dict.Contains(item);
1661+
1662+
public bool ContainsKey(TKey key) => _dict.ContainsKey(key);
1663+
1664+
public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) => throw new NotImplementedException();
1665+
1666+
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() => _dict.GetEnumerator();
1667+
1668+
public bool Remove(TKey key) => _dict.Remove(key);
1669+
1670+
public bool Remove(KeyValuePair<TKey, TValue> item) => _dict.Remove(item.Key);
1671+
1672+
public bool TryGetValue(TKey key, out TValue value) => _dict.TryGetValue(key, out value);
1673+
1674+
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => _dict.GetEnumerator();
1675+
}
1676+
1677+
public class ExtendedDictionary<TKey, TValue> : Dictionary<TKey, TValue>
1678+
{
1679+
1680+
}
16051681
}
16061682
}

0 commit comments

Comments
 (0)