@@ -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