Skip to content

Commit 316ddda

Browse files
authored
ConfigurationBinder handles ISet<> (#68133)
1 parent e47ffcb commit 316ddda

3 files changed

Lines changed: 1030 additions & 93 deletions

File tree

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

Lines changed: 183 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ public static void Bind(this IConfiguration configuration, object? instance, Act
202202
}
203203

204204
[RequiresUnreferencedCode(PropertyTrimmingWarningMessage)]
205-
private static void BindNonScalar(this IConfiguration configuration, object instance, BinderOptions options)
205+
private static void BindProperties(object instance, IConfiguration configuration, BinderOptions options)
206206
{
207207
List<PropertyInfo> modelProperties = GetAllProperties(instance.GetType());
208208

@@ -258,75 +258,6 @@ private static void BindProperty(PropertyInfo property, object instance, IConfig
258258
}
259259
}
260260

261-
[RequiresUnreferencedCode("Cannot statically analyze what the element type is of the object collection in type so its members may be trimmed.")]
262-
private static object BindToCollection(Type type, IConfiguration config, BinderOptions options)
263-
{
264-
Type genericType = typeof(List<>).MakeGenericType(type.GenericTypeArguments[0]);
265-
object instance = Activator.CreateInstance(genericType)!;
266-
BindCollection(instance, genericType, config, options);
267-
return instance;
268-
}
269-
270-
// Try to create an array/dictionary instance to back various collection interfaces
271-
[RequiresUnreferencedCode("In case type is a Dictionary, cannot statically analyze what the element type is of the value objects in the dictionary so its members may be trimmed.")]
272-
private static object? AttemptBindToCollectionInterfaces(
273-
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
274-
Type type,
275-
IConfiguration config, BinderOptions options)
276-
{
277-
if (!type.IsInterface)
278-
{
279-
return null;
280-
}
281-
282-
Type? collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyList<>), type);
283-
if (collectionInterface != null)
284-
{
285-
// IEnumerable<T> is guaranteed to have exactly one parameter
286-
return BindToCollection(type, config, options);
287-
}
288-
289-
collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyDictionary<,>), type);
290-
if (collectionInterface != null)
291-
{
292-
Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(type.GenericTypeArguments[0], type.GenericTypeArguments[1]);
293-
object instance = Activator.CreateInstance(dictionaryType)!;
294-
BindDictionary(instance, dictionaryType, config, options);
295-
return instance;
296-
}
297-
298-
collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type);
299-
if (collectionInterface != null)
300-
{
301-
object instance = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(type.GenericTypeArguments[0], type.GenericTypeArguments[1]))!;
302-
BindDictionary(instance, collectionInterface, config, options);
303-
return instance;
304-
}
305-
306-
collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyCollection<>), type);
307-
if (collectionInterface != null)
308-
{
309-
// IReadOnlyCollection<T> is guaranteed to have exactly one parameter
310-
return BindToCollection(type, config, options);
311-
}
312-
313-
collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type);
314-
if (collectionInterface != null)
315-
{
316-
// ICollection<T> is guaranteed to have exactly one parameter
317-
return BindToCollection(type, config, options);
318-
}
319-
320-
collectionInterface = FindOpenGenericInterface(typeof(IEnumerable<>), type);
321-
if (collectionInterface != null)
322-
{
323-
// IEnumerable<T> is guaranteed to have exactly one parameter
324-
return BindToCollection(type, config, options);
325-
}
326-
327-
return null;
328-
}
329-
330261
[RequiresUnreferencedCode(TrimmingWarningMessage)]
331262
private static void BindInstance(
332263
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type,
@@ -357,8 +288,8 @@ private static void BindInstance(
357288

358289
if (config != null && config.GetChildren().Any())
359290
{
360-
// for arrays and read-only list-like interfaces, we concatenate on to what is already there
361-
if (type.IsArray || IsArrayCompatibleReadOnlyInterface(type))
291+
// for arrays, collections, and read-only list-like interfaces, we concatenate on to what is already there
292+
if (type.IsArray || IsArrayCompatibleInterface(type))
362293
{
363294
if (!bindingPoint.IsReadOnly)
364295
{
@@ -367,6 +298,36 @@ private static void BindInstance(
367298
return;
368299
}
369300

301+
// for sets and read-only set interfaces, we clone what's there into a new collection.
302+
if (TypeIsASetInterface(type))
303+
{
304+
if (!bindingPoint.IsReadOnly)
305+
{
306+
object? newValue = BindSet(type, (IEnumerable?)bindingPoint.Value, config, options);
307+
if (newValue != null)
308+
{
309+
bindingPoint.SetValue(newValue);
310+
}
311+
}
312+
return;
313+
}
314+
315+
// For other mutable interfaces like ICollection<>, IDictionary<,> and ISet<>, we prefer copying values and setting them
316+
// on a new instance of the interface over populating the existing instance implementing the interface.
317+
// This has already been done, so there's not need to check again.
318+
if (TypeIsADictionaryInterface(type))
319+
{
320+
if (!bindingPoint.IsReadOnly)
321+
{
322+
object? newValue = BindDictionaryInterface(bindingPoint.Value, type, config, options);
323+
if (newValue != null)
324+
{
325+
bindingPoint.SetValue(newValue);
326+
}
327+
}
328+
return;
329+
}
330+
370331
// If we don't have an instance, try to create one
371332
if (bindingPoint.Value is null)
372333
{
@@ -376,34 +337,32 @@ private static void BindInstance(
376337
return;
377338
}
378339

379-
object? boundFromInterface = AttemptBindToCollectionInterfaces(type, config, options);
380-
if (boundFromInterface != null)
381-
{
382-
bindingPoint.SetValue(boundFromInterface);
383-
return; // We are already done if binding to a new collection instance worked
384-
}
340+
// For other mutable interfaces like ICollection<> and ISet<>, we prefer copying values and setting them
341+
// on a new instance of the interface over populating the existing instance implementing the interface.
342+
// This has already been done, so there's not need to check again. For dictionaries, we fill the existing
343+
// instance if there is one (which hasn't happened yet), and only create a new instance if necessary.
385344

386345
bindingPoint.SetValue(CreateInstance(type, config, options));
387346
}
388347

389-
// See if it's a Dictionary
390-
Type? collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type);
391-
if (collectionInterface != null)
348+
// At this point we know that we have a non-null bindingPoint.Value, we just have to populate the items
349+
// using the IDictionary<> or ICollection<> interfaces, or properties using reflection.
350+
Type? dictionaryInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type);
351+
352+
if (dictionaryInterface != null)
392353
{
393-
BindDictionary(bindingPoint.Value!, collectionInterface, config, options);
354+
BindConcreteDictionary(bindingPoint.Value!, dictionaryInterface, config, options);
394355
}
395356
else
396357
{
397-
// See if it's an ICollection
398-
collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type);
358+
Type? collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type);
399359
if (collectionInterface != null)
400360
{
401361
BindCollection(bindingPoint.Value!, collectionInterface, config, options);
402362
}
403-
// Something else
404363
else
405364
{
406-
BindNonScalar(config, bindingPoint.Value!, options);
365+
BindProperties(bindingPoint.Value!, config, options);
407366
}
408367
}
409368
}
@@ -522,8 +481,8 @@ private static bool CanBindToTheseConstructorParameters(ParameterInfo[] construc
522481
}
523482

524483
[RequiresUnreferencedCode("Cannot statically analyze what the element type is of the value objects in the dictionary so its members may be trimmed.")]
525-
private static void BindDictionary(
526-
object dictionary,
484+
private static object? BindDictionaryInterface(
485+
object? source,
527486
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)]
528487
Type dictionaryType,
529488
IConfiguration config, BinderOptions options)
@@ -533,13 +492,68 @@ private static void BindDictionary(
533492
Type valueType = dictionaryType.GenericTypeArguments[1];
534493
bool keyTypeIsEnum = keyType.IsEnum;
535494

495+
if (keyType != typeof(string) && !keyTypeIsEnum)
496+
{
497+
// We only support string and enum keys
498+
return null;
499+
}
500+
501+
Type genericType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
502+
MethodInfo addMethod = genericType.GetMethod("Add", DeclaredOnlyLookup)!;
503+
504+
Type kvpType = typeof(KeyValuePair<,>).MakeGenericType(keyType, valueType);
505+
PropertyInfo keyMethod = kvpType.GetProperty("Key", DeclaredOnlyLookup)!;
506+
PropertyInfo valueMethod = kvpType.GetProperty("Value", DeclaredOnlyLookup)!;
507+
508+
object dictionary = Activator.CreateInstance(genericType)!;
509+
510+
var orig = source as IEnumerable;
511+
object?[] arguments = new object?[2];
512+
513+
if (orig != null)
514+
{
515+
foreach (object? item in orig)
516+
{
517+
object? k = keyMethod.GetMethod!.Invoke(item, null);
518+
object? v = valueMethod.GetMethod!.Invoke(item, null);
519+
arguments[0] = k;
520+
arguments[1] = v;
521+
addMethod.Invoke(dictionary, arguments);
522+
}
523+
}
524+
525+
BindConcreteDictionary(dictionary, dictionaryType, config, options);
526+
527+
return dictionary;
528+
}
529+
530+
// Binds and potentially overwrites a concrete dictionary.
531+
// This differs from BindDictionaryInterface because this method doesn't clone
532+
// the dictionary; it sets and/or overwrites values directly.
533+
// When a user specifies a concrete dictionary in their config class, then that
534+
// value is used as-us. When a user specifies an interface (instantiated) in their config class,
535+
// then it is cloned to a new dictionary, the same way as other collections.
536+
[RequiresUnreferencedCode("Cannot statically analyze what the element type is of the value objects in the dictionary so its members may be trimmed.")]
537+
private static void BindConcreteDictionary(
538+
object? dictionary,
539+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)]
540+
Type dictionaryType,
541+
IConfiguration config, BinderOptions options)
542+
{
543+
Type keyType = dictionaryType.GenericTypeArguments[0];
544+
Type valueType = dictionaryType.GenericTypeArguments[1];
545+
bool keyTypeIsEnum = keyType.IsEnum;
546+
536547
if (keyType != typeof(string) && !keyTypeIsEnum)
537548
{
538549
// We only support string and enum keys
539550
return;
540551
}
552+
553+
Type genericType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType);
554+
541555
MethodInfo tryGetValue = dictionaryType.GetMethod("TryGetValue")!;
542-
PropertyInfo setter = dictionaryType.GetProperty("Item", DeclaredOnlyLookup)!;
556+
PropertyInfo setter = genericType.GetProperty("Item", DeclaredOnlyLookup)!;
543557
foreach (IConfigurationSection child in config.GetChildren())
544558
{
545559
try
@@ -548,7 +562,7 @@ private static void BindDictionary(
548562
var valueBindingPoint = new BindingPoint(
549563
initialValueProvider: () =>
550564
{
551-
var tryGetValueArgs = new object?[] { key, null };
565+
object?[] tryGetValueArgs = { key, null };
552566
return (bool)tryGetValue.Invoke(dictionary, tryGetValueArgs)! ? tryGetValueArgs[1] : null;
553567
},
554568
isReadOnly: false);
@@ -652,6 +666,62 @@ private static Array BindArray(Type type, IEnumerable? source, IConfiguration co
652666
return result;
653667
}
654668

669+
[RequiresUnreferencedCode("Cannot statically analyze what the element type is of the Array so its members may be trimmed.")]
670+
private static object? BindSet(Type type, IEnumerable? source, IConfiguration config, BinderOptions options)
671+
{
672+
Type elementType = type.GetGenericArguments()[0];
673+
674+
Type keyType = type.GenericTypeArguments[0];
675+
676+
bool keyTypeIsEnum = keyType.IsEnum;
677+
678+
if (keyType != typeof(string) && !keyTypeIsEnum)
679+
{
680+
// We only support string and enum keys
681+
return null;
682+
}
683+
684+
Type genericType = typeof(HashSet<>).MakeGenericType(keyType);
685+
object instance = Activator.CreateInstance(genericType)!;
686+
687+
MethodInfo addMethod = genericType.GetMethod("Add", DeclaredOnlyLookup)!;
688+
689+
object?[] arguments = new object?[1];
690+
691+
if (source != null)
692+
{
693+
foreach (object? item in source)
694+
{
695+
arguments[0] = item;
696+
addMethod.Invoke(instance, arguments);
697+
}
698+
}
699+
700+
foreach (IConfigurationSection section in config.GetChildren())
701+
{
702+
var itemBindingPoint = new BindingPoint();
703+
try
704+
{
705+
BindInstance(
706+
type: elementType,
707+
bindingPoint: itemBindingPoint,
708+
config: section,
709+
options: options);
710+
if (itemBindingPoint.HasNewValue)
711+
{
712+
arguments[0] = itemBindingPoint.Value;
713+
714+
addMethod.Invoke(instance, arguments);
715+
}
716+
}
717+
catch
718+
{
719+
}
720+
}
721+
722+
return instance;
723+
}
724+
655725
[RequiresUnreferencedCode(TrimmingWarningMessage)]
656726
private static bool TryConvertValue(
657727
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
@@ -719,16 +789,38 @@ private static bool TryConvertValue(
719789
return result;
720790
}
721791

722-
private static bool IsArrayCompatibleReadOnlyInterface(Type type)
792+
private static bool TypeIsADictionaryInterface(Type type)
793+
{
794+
if (!type.IsInterface || !type.IsConstructedGenericType) { return false; }
795+
796+
Type genericTypeDefinition = type.GetGenericTypeDefinition();
797+
return genericTypeDefinition == typeof(IDictionary<,>)
798+
|| genericTypeDefinition == typeof(IReadOnlyDictionary<,>);
799+
}
800+
801+
private static bool IsArrayCompatibleInterface(Type type)
723802
{
724803
if (!type.IsInterface || !type.IsConstructedGenericType) { return false; }
725804

726805
Type genericTypeDefinition = type.GetGenericTypeDefinition();
727806
return genericTypeDefinition == typeof(IEnumerable<>)
807+
|| genericTypeDefinition == typeof(ICollection<>)
728808
|| genericTypeDefinition == typeof(IReadOnlyCollection<>)
729809
|| genericTypeDefinition == typeof(IReadOnlyList<>);
730810
}
731811

812+
private static bool TypeIsASetInterface(Type type)
813+
{
814+
if (!type.IsInterface || !type.IsConstructedGenericType) { return false; }
815+
816+
Type genericTypeDefinition = type.GetGenericTypeDefinition();
817+
return genericTypeDefinition == typeof(ISet<>)
818+
#if NETCOREAPP
819+
|| genericTypeDefinition == typeof(IReadOnlySet<>)
820+
#endif
821+
;
822+
}
823+
732824
private static Type? FindOpenGenericInterface(
733825
Type expected,
734826
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)]

0 commit comments

Comments
 (0)