Support remaining collections in config binder generator (#86285)
authorLayomi Akinrinade <laakinri@microsoft.com>
Tue, 16 May 2023 23:15:26 +0000 (16:15 -0700)
committerGitHub <noreply@github.com>
Tue, 16 May 2023 23:15:26 +0000 (16:15 -0700)
* Support remaining collections in config binder generator

* Address feedback

* Fix IReadOnlyDictionary net462 issue

13 files changed:
src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/gen/KnownTypeSymbols.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/CollectionSpec.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/ConstructionStrategy.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/ObjectSpec.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/PropertySpec.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Collections.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.Collections.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestCollectionsGen.generated.txt [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs

index d68c5c8..33b5295 100644 (file)
@@ -606,11 +606,6 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
             {
                 switch (type.SpecKind)
                 {
-                    case TypeSpecKind.Array:
-                        {
-                            EmitBindCoreImplForArray((ArraySpec)type);
-                        }
-                        break;
                     case TypeSpecKind.Enumerable:
                         {
                             EmitBindCoreImplForEnumerable((EnumerableSpec)type);
@@ -643,11 +638,23 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                 }
             }
 
-            private void EmitBindCoreImplForArray(ArraySpec type)
+            private void EmitBindCoreImplForEnumerable(EnumerableSpec type)
             {
-                EnumerableSpec concreteType = (EnumerableSpec)type.ConcreteType;
+                EmitCheckForNullArgument_WithBlankLine_IfRequired(type.IsValueType);
+
+                if (type.PopulationStrategy is CollectionPopulationStrategy.Array)
+                {
+                    EmitPopulationImplForArray(type);
+                }
+                else
+                {
+                    EmitPopulationImplForEnumerableWithAdd(type);
+                }
+            }
 
-                EmitCheckForNullArgument_WithBlankLine_IfRequired(isValueType: false);
+            private void EmitPopulationImplForArray(EnumerableSpec type)
+            {
+                EnumerableSpec concreteType = (EnumerableSpec)type.ConcreteType;
 
                 // Create, bind, and add elements to temp list.
                 string tempVarName = GetIncrementalVarName(Identifier.temp);
@@ -661,15 +668,15 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                     """);
             }
 
-            private void EmitBindCoreImplForEnumerable(EnumerableSpec type)
+            private void EmitPopulationImplForEnumerableWithAdd(EnumerableSpec type)
             {
-                EmitCheckForNullArgument_WithBlankLine_IfRequired(type.IsValueType);
-
                 TypeSpec elementType = type.ElementType;
 
+                EmitCollectionCastIfRequired(type, out string objIdentifier);
+
                 _writer.WriteBlockStart($"foreach ({Identifier.IConfigurationSection} {Identifier.section} in {Identifier.configuration}.{Identifier.GetChildren}())");
 
-                string addStatement = $"{Identifier.obj}.{Identifier.Add}({Identifier.element})";
+                string addExpression = $"{objIdentifier}.{Identifier.Add}({Identifier.element})";
 
                 if (elementType.SpecKind is TypeSpecKind.ParsableFromString)
                 {
@@ -678,19 +685,19 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                     {
                         string tempVarName = GetIncrementalVarName(Identifier.stringValue);
                         _writer.WriteBlockStart($"if ({Expression.sectionValue} is string {tempVarName})");
-                        _writer.WriteLine($"{Identifier.obj}.{Identifier.Add}({tempVarName});");
+                        _writer.WriteLine($"{objIdentifier}.{Identifier.Add}({tempVarName});");
                         _writer.WriteBlockEnd();
                     }
                     else
                     {
                         EmitVarDeclaration(elementType, Identifier.element);
-                        EmitBindLogicFromString(stringParsableType, Identifier.element, Expression.sectionValue, Expression.sectionPath, () => _writer.WriteLine($"{addStatement};"));
+                        EmitBindLogicFromString(stringParsableType, Identifier.element, Expression.sectionValue, Expression.sectionPath, () => _writer.WriteLine($"{addExpression};"));
                     }
                 }
                 else
                 {
                     EmitBindCoreCall(elementType, Identifier.element, Identifier.section, InitializationKind.Declaration);
-                    _writer.WriteLine($"{addStatement};");
+                    _writer.WriteLine($"{addExpression};");
                 }
 
                 _writer.WriteBlockEnd();
@@ -700,11 +707,14 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
             {
                 EmitCheckForNullArgument_WithBlankLine_IfRequired(type.IsValueType);
 
+                EmitCollectionCastIfRequired(type, out string objIdentifier);
+
                 _writer.WriteBlockStart($"foreach ({Identifier.IConfigurationSection} {Identifier.section} in {Identifier.configuration}.{Identifier.GetChildren}())");
 
-                // Parse key
                 ParsableFromStringTypeSpec keyType = type.KeyType;
+                TypeSpec elementType = type.ElementType;
 
+                // Parse key
                 if (keyType.StringParsableTypeKind is StringParsableTypeKind.ConfigValue)
                 {
                     _writer.WriteLine($"{keyType.MinimalDisplayString} {Identifier.key} = {Expression.sectionKey};");
@@ -723,8 +733,6 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
 
                 void Emit_BindAndAddLogic_ForElement()
                 {
-                    TypeSpec elementType = type.ElementType;
-
                     if (elementType.SpecKind == TypeSpecKind.ParsableFromString)
                     {
                         ParsableFromStringTypeSpec stringParsableType = (ParsableFromStringTypeSpec)elementType;
@@ -732,7 +740,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                         {
                             string tempVarName = GetIncrementalVarName(Identifier.stringValue);
                             _writer.WriteBlockStart($"if ({Expression.sectionValue} is string {tempVarName})");
-                            _writer.WriteLine($"{Identifier.obj}[{Identifier.key}] = {tempVarName};");
+                            _writer.WriteLine($"{objIdentifier}[{Identifier.key}] = {tempVarName};");
                             _writer.WriteBlockEnd();
                         }
                         else
@@ -743,25 +751,50 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                                 Identifier.element,
                                 Expression.sectionValue,
                                 Expression.sectionPath,
-                                () => _writer.WriteLine($"{Identifier.obj}[{Identifier.key}] = {Identifier.element};"));
+                                () => _writer.WriteLine($"{objIdentifier}[{Identifier.key}] = {Identifier.element};"));
                         }
                     }
                     else // For complex types:
                     {
+                        bool isValueType = elementType.IsValueType;
+                        string expressionForElementIsNotNull = $"{Identifier.element} is not null";
                         string elementTypeDisplayString = elementType.MinimalDisplayString + (elementType.IsValueType ? string.Empty : "?");
 
-                        // If key already exists, bind to value to existing element instance if not null (for ref types).
-                        string conditionToUseExistingElement = $"{Identifier.obj}.{Identifier.TryGetValue}({Identifier.key}, out {elementTypeDisplayString} {Identifier.element})";
-                        if (!elementType.IsValueType)
+                        string expressionForElementExists = $"{objIdentifier}.{Identifier.TryGetValue}({Identifier.key}, out {elementTypeDisplayString} {Identifier.element})";
+                        string conditionToUseExistingElement = expressionForElementExists;
+
+                        // If key already exists, bind to existing element instance if not null (for ref types).
+                        if (!isValueType)
                         {
-                            conditionToUseExistingElement += $" && {Identifier.element} is not null";
+                            conditionToUseExistingElement += $" && {expressionForElementIsNotNull}";
                         }
+
                         _writer.WriteBlockStart($"if (!({conditionToUseExistingElement}))");
                         EmitObjectInit(elementType, Identifier.element, InitializationKind.SimpleAssignment);
                         _writer.WriteBlockEnd();
 
+                        if (elementType is CollectionSpec
+                            {
+                                ConstructionStrategy: ConstructionStrategy.ParameterizedConstructor or ConstructionStrategy.ToEnumerableMethod
+                            } collectionSpec)
+                        {
+                            // This is a read-only collection. If the element exists and is not null,
+                            // we need to copy its contents into a new instance & then append/bind to that.
+
+                            string initExpression = collectionSpec.ConstructionStrategy is ConstructionStrategy.ParameterizedConstructor
+                                ? $"new {collectionSpec.ConcreteType.MinimalDisplayString}({Identifier.element})"
+                                : $"{Identifier.element}.{collectionSpec.ToEnumerableMethodCall!}";
+
+                            _writer.WriteBlock($$"""
+                                else
+                                {
+                                    {{Identifier.element}} = {{initExpression}};
+                                }
+                                """);
+                        }
+
                         EmitBindCoreCall(elementType, $"{Identifier.element}!", Identifier.section, InitializationKind.None);
-                        _writer.WriteLine($"{Identifier.obj}[{Identifier.key}] = {Identifier.element};");
+                        _writer.WriteLine($"{objIdentifier}[{Identifier.key}] = {Identifier.element};");
                     }
                 }
 
@@ -788,9 +821,11 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                 {
                     _writer.WriteBlockStart($@"case ""{property.ConfigurationKeyName}"":");
 
-                    TypeSpec propertyType = property.Type;
+                    if (property.ShouldBind())
+                    {
+                        EmitBindCoreImplForProperty(property, property.Type!, parentType: type);
+                    }
 
-                    EmitBindCoreImplForProperty(property, propertyType, parentType: type);
                     _writer.WriteBlockEnd();
                     _writer.WriteLine("break;");
                 }
@@ -870,10 +905,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                         break;
                     default:
                         {
-                            EmitBindCoreCallForProperty(
-                                property,
-                                propertyType,
-                                expressionForPropertyAccess);
+                            EmitBindCoreCallForProperty(property, propertyType, expressionForPropertyAccess);
                         }
                         break;
                 }
@@ -1034,25 +1066,33 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                     return;
                 }
 
-                string displayString = GetTypeDisplayString(type);
+                string expressionForInit;
+                CollectionSpec? collectionType = type as CollectionSpec;
 
-                string expressionForInit = null;
-                if (type is ArraySpec)
+                string displayString;
+                if (collectionType is not null)
                 {
-                    expressionForInit = $"new {_arrayBracketsRegex.Replace(displayString, "[0]", 1)}";
+                    if (collectionType is EnumerableSpec { PopulationStrategy: CollectionPopulationStrategy.Array })
+                    {
+                        displayString = GetTypeDisplayString(type);
+                        expressionForInit = $"new {_arrayBracketsRegex.Replace(displayString, "[0]", 1)}";
+                    }
+                    else
+                    {
+                        displayString = GetTypeDisplayString(collectionType.ConcreteType ?? collectionType);
+                        expressionForInit = $"new {displayString}()";
+                    }
                 }
-                else if (type.ConstructionStrategy != ConstructionStrategy.ParameterlessConstructor)
+                else if (type.ConstructionStrategy is ConstructionStrategy.ParameterlessConstructor)
                 {
-                    return;
+                    displayString = GetTypeDisplayString(type);
+                    expressionForInit = $"new {displayString}()";
                 }
-                else if (type is CollectionSpec { ConcreteType: { } concreteType })
+                else
                 {
-                    displayString = GetTypeDisplayString(concreteType);
+                    return;
                 }
 
-                // Not an array.
-                expressionForInit ??= $"new {displayString}()";
-
                 if (initKind == InitializationKind.Declaration)
                 {
                     Debug.Assert(!expressionForMemberAccess.Contains("."));
@@ -1060,7 +1100,19 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                 }
                 else if (initKind == InitializationKind.AssignmentWithNullCheck)
                 {
-                    _writer.WriteLine($"{expressionForMemberAccess} ??= {expressionForInit};");
+                    ConstructionStrategy? collectionConstructionStratey = collectionType?.ConstructionStrategy;
+                    if (collectionConstructionStratey is ConstructionStrategy.ParameterizedConstructor)
+                    {
+                        _writer.WriteLine($"{expressionForMemberAccess} = {expressionForMemberAccess} is null ? {expressionForInit} : new {displayString}({expressionForMemberAccess});");
+                    }
+                    else if (collectionConstructionStratey is ConstructionStrategy.ToEnumerableMethod)
+                    {
+                        _writer.WriteLine($"{expressionForMemberAccess} = {expressionForMemberAccess} is null ? {expressionForInit} : {expressionForMemberAccess}.{collectionType.ToEnumerableMethodCall!};");
+                    }
+                    else
+                    {
+                        _writer.WriteLine($"{expressionForMemberAccess} ??= {expressionForInit};");
+                    }
                 }
                 else
                 {
@@ -1068,6 +1120,22 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                 }
             }
 
+            private void EmitCollectionCastIfRequired(CollectionSpec type, out string objIdentifier)
+            {
+                objIdentifier = Identifier.obj;
+                if (type.PopulationStrategy is CollectionPopulationStrategy.Cast_Then_Add)
+                {
+                    objIdentifier = Identifier.temp;
+                    _writer.WriteBlock($$"""
+                        if ({{Identifier.obj}} is not {{type.PopulationCastType!.MinimalDisplayString}} {{objIdentifier}})
+                        {
+                            return;
+                        }
+                        """);
+                    _writer.WriteBlankLine();
+                }
+            }
+
             private void EmitCastToIConfigurationSection()
             {
                 string sectionTypeDisplayString;
index b8d5dec..c92bbc0 100644 (file)
@@ -11,7 +11,6 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
         internal sealed class Helpers
         {
             public static DiagnosticDescriptor TypeNotSupported { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.TypeNotSupported));
-            public static DiagnosticDescriptor AbstractOrInterfaceNotSupported { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.AbstractOrInterfaceNotSupported));
             public static DiagnosticDescriptor NeedPublicParameterlessConstructor { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.NeedPublicParameterlessConstructor));
             public static DiagnosticDescriptor CollectionNotSupported { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.CollectionNotSupported));
             public static DiagnosticDescriptor DictionaryKeyNotSupported { get; } = CreateTypeNotSupportedDescriptor(nameof(SR.DictionaryKeyNotSupported));
index 45d71cd..e44c776 100644 (file)
@@ -7,6 +7,7 @@ using System.Collections.Immutable;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
+using System.Reflection.Metadata;
 using Microsoft.CodeAnalysis;
 using Microsoft.CodeAnalysis.Operations;
 
@@ -328,8 +329,8 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                 TypeSpec? spec = GetOrCreateTypeSpec(type, location);
                 if (spec != null)
                 {
-                    GetRootConfigTypeCache(method).Add(spec);
-                    GetRootConfigTypeCache(methodGroup).Add(spec);
+                    AddToRootConfigTypeCache(method, spec);
+                    AddToRootConfigTypeCache(methodGroup, spec);
 
                     _methodsToGen |= method;
                 }
@@ -404,20 +405,22 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                 {
                     if (spec is not null)
                     {
-                        GetRootConfigTypeCache(MethodSpecifier.BindCore).Add(spec);
+                        AddToRootConfigTypeCache(MethodSpecifier.BindCore, spec);
                         _methodsToGen |= MethodSpecifier.BindCore;
                     }
                 }
             }
 
-            private HashSet<TypeSpec> GetRootConfigTypeCache(MethodSpecifier method)
+            private void AddToRootConfigTypeCache(MethodSpecifier method, TypeSpec spec)
             {
+                Debug.Assert(spec is not null);
+
                 if (!_rootConfigTypes.TryGetValue(method, out HashSet<TypeSpec> types))
                 {
                     _rootConfigTypes[method] = types = new HashSet<TypeSpec>();
                 }
 
-                return types;
+                types.Add(spec);
             }
 
             private static bool IsNullable(ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? underlyingType)
@@ -542,7 +545,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
             {
                 spec = GetOrCreateTypeSpec(type);
 
-                if (spec == null)
+                if (spec is null)
                 {
                     ReportUnsupportedType(type, descriptor);
                     return false;
@@ -551,7 +554,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                 return true;
             }
 
-            private ArraySpec? CreateArraySpec(IArrayTypeSymbol arrayType, Location? location)
+            private EnumerableSpec? CreateArraySpec(IArrayTypeSymbol arrayType, Location? location)
             {
                 if (!TryGetTypeSpec(arrayType.ElementType, Helpers.ElementTypeNotSupported, out TypeSpec elementSpec))
                 {
@@ -559,15 +562,21 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                 }
 
                 // We want a BindCore method for List<TElement> as a temp holder for the array values.
-                EnumerableSpec? listSpec = ConstructAndCacheGenericTypeForBindCore(_typeSymbols.List, arrayType.ElementType) as EnumerableSpec;
+                EnumerableSpec? listSpec = GetOrCreateTypeSpec(_typeSymbols.List.Construct(arrayType.ElementType)) as EnumerableSpec;
                 // We know the element type is supported.
                 Debug.Assert(listSpec != null);
+                if (listSpec is not null)
+                {
+                    AddToRootConfigTypeCache(MethodSpecifier.BindCore, listSpec);
+                }
 
-                return new ArraySpec(arrayType)
+                return new EnumerableSpec(arrayType)
                 {
                     Location = location,
                     ElementType = elementSpec,
                     ConcreteType = listSpec,
+                    PopulationStrategy = CollectionPopulationStrategy.Array,
+                    ToEnumerableMethodCall = null,
                 };
             }
 
@@ -593,12 +602,8 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                 {
                     return CreateDictionarySpec(type, location, keyType, elementType);
                 }
-                else if (IsCandidateEnumerable(type, out elementType))
-                {
-                    return CreateEnumerableSpec(type, location, elementType);
-                }
 
-                return null;
+                return CreateEnumerableSpec(type, location);
             }
 
             private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? location, ITypeSymbol keyType, ITypeSymbol elementType)
@@ -615,54 +620,136 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                     return null;
                 }
 
-                DictionarySpec? concreteType = null;
-                if (IsInterfaceMatch(type, _typeSymbols.GenericIDictionary) || IsInterfaceMatch(type, _typeSymbols.IDictionary))
+                ConstructionStrategy constructionStrategy;
+                CollectionPopulationStrategy populationStrategy;
+                INamedTypeSymbol? concreteType = null;
+                INamedTypeSymbol? populationCastType = null;
+                string? toEnumerableMethodCall = null;
+
+                if (HasPublicParameterlessCtor(type))
+                {
+                    constructionStrategy = ConstructionStrategy.ParameterlessConstructor;
+
+                    if (HasAddMethod(type, keyType, elementType))
+                    {
+                        populationStrategy = CollectionPopulationStrategy.Add;
+                    }
+                    else if (GetInterface(type, _typeSymbols.GenericIDictionary_Unbound) is not null)
+                    {
+                        populationCastType = _typeSymbols.GenericIDictionary;
+                        populationStrategy = CollectionPopulationStrategy.Cast_Then_Add;
+                    }
+                    else
+                    {
+                        ReportUnsupportedType(type, Helpers.CollectionNotSupported, location);
+                        return null;
+                    }
+                }
+                else if (IsInterfaceMatch(type, _typeSymbols.GenericIDictionary_Unbound) || IsInterfaceMatch(type, _typeSymbols.IDictionary))
+                {
+                    concreteType = _typeSymbols.Dictionary;
+                    constructionStrategy = ConstructionStrategy.ParameterlessConstructor;
+                    populationStrategy = CollectionPopulationStrategy.Add;
+                }
+                else if (IsInterfaceMatch(type, _typeSymbols.IReadOnlyDictionary_Unbound))
                 {
-                    // We know the key and element types are supported.
-                    concreteType = ConstructAndCacheGenericTypeForBindCore(_typeSymbols.Dictionary, keyType, elementType) as DictionarySpec;
-                    Debug.Assert(concreteType != null);
+                    concreteType = _typeSymbols.Dictionary;
+                    populationCastType = _typeSymbols.GenericIDictionary;
+                    constructionStrategy = ConstructionStrategy.ToEnumerableMethod;
+                    populationStrategy = CollectionPopulationStrategy.Cast_Then_Add;
+                    toEnumerableMethodCall = "ToDictionary(pair => pair.Key, pair => pair.Value)";
+                    _namespaces.Add("System.Linq");
                 }
-                else if (!CanConstructObject(type, location) || !HasAddMethod(type, elementType, keyType))
+                else
                 {
                     ReportUnsupportedType(type, Helpers.CollectionNotSupported, location);
                     return null;
                 }
 
-                return new DictionarySpec(type)
+                DictionarySpec spec = new(type)
                 {
                     Location = location,
                     KeyType = (ParsableFromStringTypeSpec)keySpec,
                     ElementType = elementSpec,
-                    ConstructionStrategy = ConstructionStrategy.ParameterlessConstructor,
-                    ConcreteType = concreteType
+                    ConstructionStrategy = constructionStrategy,
+                    PopulationStrategy = populationStrategy,
+                    ToEnumerableMethodCall = toEnumerableMethodCall,
                 };
-            }
 
-            private TypeSpec? ConstructAndCacheGenericTypeForBindCore(INamedTypeSymbol type, params ITypeSymbol[] parameters)
-            {
-                Debug.Assert(type.IsGenericType);
-                TypeSpec spec = GetOrCreateTypeSpec(type.Construct(parameters));
-                GetRootConfigTypeCache(MethodSpecifier.BindCore).Add(spec);
+                Debug.Assert(!(populationStrategy is CollectionPopulationStrategy.Cast_Then_Add && populationCastType is null));
+                spec.ConcreteType = ConstructGenericCollectionTypeSpec(concreteType, keyType, elementType);
+                spec.PopulationCastType = ConstructGenericCollectionTypeSpec(populationCastType, keyType, elementType);
+
                 return spec;
             }
 
-            private EnumerableSpec? CreateEnumerableSpec(INamedTypeSymbol type, Location? location, ITypeSymbol elementType)
+            private EnumerableSpec? CreateEnumerableSpec(INamedTypeSymbol type, Location? location)
             {
-                if (!TryGetTypeSpec(elementType, Helpers.ElementTypeNotSupported, out TypeSpec elementSpec))
+                if (!TryGetElementType(type, out ITypeSymbol? elementType) ||
+                    !TryGetTypeSpec(elementType, Helpers.ElementTypeNotSupported, out TypeSpec elementSpec))
                 {
                     return null;
                 }
-                EnumerableSpec? concreteType = null;
-                if (IsInterfaceMatch(type, _typeSymbols.ISet))
+
+                ConstructionStrategy constructionStrategy;
+                CollectionPopulationStrategy populationStrategy;
+                INamedTypeSymbol? concreteType = null;
+                INamedTypeSymbol? populationCastType = null;
+
+                if (HasPublicParameterlessCtor(type))
+                {
+                    constructionStrategy = ConstructionStrategy.ParameterlessConstructor;
+
+                    if (HasAddMethod(type, elementType))
+                    {
+                        populationStrategy = CollectionPopulationStrategy.Add;
+                    }
+                    else if (GetInterface(type, _typeSymbols.GenericICollection_Unbound) is not null)
+                    {
+                        populationCastType = _typeSymbols.GenericICollection;
+                        populationStrategy = CollectionPopulationStrategy.Cast_Then_Add;
+                    }
+                    else
+                    {
+                        ReportUnsupportedType(type, Helpers.CollectionNotSupported, location);
+                        return null;
+                    }
+                }
+                else if (IsInterfaceMatch(type, _typeSymbols.GenericICollection_Unbound) ||
+                    IsInterfaceMatch(type, _typeSymbols.GenericIList_Unbound))
+                {
+                    concreteType = _typeSymbols.List;
+                    constructionStrategy = ConstructionStrategy.ParameterlessConstructor;
+                    populationStrategy = CollectionPopulationStrategy.Add;
+                }
+                else if (IsInterfaceMatch(type, _typeSymbols.GenericIEnumerable_Unbound))
+                {
+                    concreteType = _typeSymbols.List;
+                    populationCastType = _typeSymbols.GenericICollection;
+                    constructionStrategy = ConstructionStrategy.ParameterizedConstructor;
+                    populationStrategy = CollectionPopulationStrategy.Cast_Then_Add;
+                }
+                else if (IsInterfaceMatch(type, _typeSymbols.ISet_Unbound))
                 {
-                    concreteType = ConstructAndCacheGenericTypeForBindCore(_typeSymbols.HashSet, elementType) as EnumerableSpec;
+                    concreteType = _typeSymbols.HashSet;
+                    constructionStrategy = ConstructionStrategy.ParameterlessConstructor;
+                    populationStrategy = CollectionPopulationStrategy.Add;
                 }
-                else if (IsInterfaceMatch(type, _typeSymbols.ICollection) ||
-                    IsInterfaceMatch(type, _typeSymbols.GenericIList))
+                else if (IsInterfaceMatch(type, _typeSymbols.IReadOnlySet_Unbound))
                 {
-                    concreteType = ConstructAndCacheGenericTypeForBindCore(_typeSymbols.List, elementType) as EnumerableSpec;
+                    concreteType = _typeSymbols.HashSet;
+                    populationCastType = _typeSymbols.ISet;
+                    constructionStrategy = ConstructionStrategy.ParameterizedConstructor;
+                    populationStrategy = CollectionPopulationStrategy.Cast_Then_Add;
                 }
-                else if (!CanConstructObject(type, location) || !HasAddMethod(type, elementType))
+                else if (IsInterfaceMatch(type, _typeSymbols.IReadOnlyList_Unbound) || IsInterfaceMatch(type, _typeSymbols.IReadOnlyCollection_Unbound))
+                {
+                    concreteType = _typeSymbols.List;
+                    populationCastType = _typeSymbols.GenericICollection;
+                    constructionStrategy = ConstructionStrategy.ParameterizedConstructor;
+                    populationStrategy = CollectionPopulationStrategy.Cast_Then_Add;
+                }
+                else
                 {
                     ReportUnsupportedType(type, Helpers.CollectionNotSupported, location);
                     return null;
@@ -670,13 +757,20 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
 
                 RegisterHasChildrenHelperForGenIfRequired(elementSpec);
 
-                return new EnumerableSpec(type)
+                EnumerableSpec spec = new(type)
                 {
                     Location = location,
                     ElementType = elementSpec,
-                    ConstructionStrategy = ConstructionStrategy.ParameterlessConstructor,
-                    ConcreteType = concreteType
+                    ConstructionStrategy = constructionStrategy,
+                    PopulationStrategy = populationStrategy,
+                    ToEnumerableMethodCall = null,
                 };
+
+                Debug.Assert(!(populationStrategy is CollectionPopulationStrategy.Cast_Then_Add && populationCastType is null));
+                spec.ConcreteType = ConstructGenericCollectionTypeSpec(concreteType, elementType);
+                spec.PopulationCastType = ConstructGenericCollectionTypeSpec(populationCastType, elementType);
+
+                return spec;
             }
 
             private ObjectSpec? CreateObjectSpec(INamedTypeSymbol type, Location? location)
@@ -684,9 +778,9 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                 Debug.Assert(!_createdSpecs.ContainsKey(type));
 
                 // Add spec to cache before traversing properties to avoid stack overflow.
-
-                if (!CanConstructObject(type, location))
+                if (!HasPublicParameterlessCtor(type))
                 {
+                    ReportUnsupportedType(type, Helpers.NeedPublicParameterlessConstructor, location);
                     _createdSpecs.Add(type, null);
                     return null;
                 }
@@ -702,8 +796,12 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                         {
                             if (property.Type is ITypeSymbol { } propertyType)
                             {
-                                TypeSpec? propertyTypeSpec = GetOrCreateTypeSpec(propertyType);
+                                AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => Helpers.TypesAreEqual(a.AttributeClass, _typeSymbols.ConfigurationKeyNameAttribute));
                                 string propertyName = property.Name;
+                                string configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName;
+
+                                TypeSpec? propertyTypeSpec = GetOrCreateTypeSpec(propertyType);
+                                PropertySpec spec;
 
                                 if (propertyTypeSpec is null)
                                 {
@@ -711,17 +809,12 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                                 }
                                 else
                                 {
-                                    AttributeData? attributeData = property.GetAttributes().FirstOrDefault(a => Helpers.TypesAreEqual(a.AttributeClass, _typeSymbols.ConfigurationKeyNameAttribute));
-                                    string configKeyName = attributeData?.ConstructorArguments.FirstOrDefault().Value as string ?? propertyName;
-
-                                    PropertySpec spec = new PropertySpec(property) { Type = propertyTypeSpec, ConfigurationKeyName = configKeyName };
-                                    if (spec.CanGet || spec.CanSet)
-                                    {
-                                        objectSpec.Properties[configKeyName] = (spec);
-                                    }
-
                                     RegisterHasChildrenHelperForGenIfRequired(propertyTypeSpec);
                                 }
+
+
+                                spec = new PropertySpec(property) { Type = propertyTypeSpec, ConfigurationKeyName = configKeyName };
+                                objectSpec.Properties[configKeyName] = spec;
                             }
                         }
                     }
@@ -742,13 +835,13 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                 }
             }
 
-            private bool IsCandidateEnumerable(INamedTypeSymbol type, out ITypeSymbol? elementType)
+            private bool TryGetElementType(INamedTypeSymbol type, out ITypeSymbol? elementType)
             {
-                INamedTypeSymbol? @interface = GetInterface(type, _typeSymbols.ICollection);
+                INamedTypeSymbol? candidate = GetInterface(type, _typeSymbols.GenericIEnumerable_Unbound);
 
-                if (@interface is not null)
+                if (candidate is not null)
                 {
-                    elementType = @interface.TypeArguments[0];
+                    elementType = candidate.TypeArguments[0];
                     return true;
                 }
 
@@ -758,11 +851,12 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
 
             private bool IsCandidateDictionary(INamedTypeSymbol type, out ITypeSymbol? keyType, out ITypeSymbol? elementType)
             {
-                INamedTypeSymbol? @interface = GetInterface(type, _typeSymbols.GenericIDictionary);
-                if (@interface is not null)
+                INamedTypeSymbol? candidate = GetInterface(type, _typeSymbols.GenericIDictionary_Unbound) ?? GetInterface(type, _typeSymbols.IReadOnlyDictionary_Unbound);
+
+                if (candidate is not null)
                 {
-                    keyType = @interface.TypeArguments[0];
-                    elementType = @interface.TypeArguments[1];
+                    keyType = candidate.TypeArguments[0];
+                    elementType = candidate.TypeArguments[1];
                     return true;
                 }
 
@@ -828,24 +922,13 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                 return false;
             }
 
-            private bool CanConstructObject(INamedTypeSymbol type, Location? location)
+            private static bool HasPublicParameterlessCtor(INamedTypeSymbol type)
             {
                 if (type.IsAbstract || type.TypeKind == TypeKind.Interface)
                 {
-                    ReportUnsupportedType(type, Helpers.AbstractOrInterfaceNotSupported, location);
-                    return false;
-                }
-                else if (!HasPublicParameterlessCtor(type))
-                {
-                    ReportUnsupportedType(type, Helpers.NeedPublicParameterlessConstructor, location);
                     return false;
                 }
 
-                return true;
-            }
-
-            private static bool HasPublicParameterlessCtor(ITypeSymbol type)
-            {
                 if (type is not INamedTypeSymbol namedType)
                 {
                     return false;
@@ -878,7 +961,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
                 return false;
             }
 
-            private static bool HasAddMethod(INamedTypeSymbol type, ITypeSymbol element, ITypeSymbol key)
+            private static bool HasAddMethod(INamedTypeSymbol type, ITypeSymbol key, ITypeSymbol element)
             {
                 INamedTypeSymbol current = type;
                 while (current != null)
@@ -897,6 +980,16 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
 
             private static bool IsEnum(ITypeSymbol type) => type is INamedTypeSymbol { EnumUnderlyingType: INamedTypeSymbol { } };
 
+            private CollectionSpec? ConstructGenericCollectionTypeSpec(INamedTypeSymbol? collectionType, params ITypeSymbol[] parameters) =>
+                (collectionType is not null ? ConstructGenericCollectionSpec(collectionType, parameters) : null);
+
+            private CollectionSpec? ConstructGenericCollectionSpec(INamedTypeSymbol type, params ITypeSymbol[] parameters)
+            {
+                Debug.Assert(type.IsGenericType);
+                INamedTypeSymbol constructedType = type.Construct(parameters);
+                return CreateCollectionSpec(constructedType, location: null);
+            }
+
             private void ReportUnsupportedType(ITypeSymbol type, DiagnosticDescriptor descriptor, Location? location = null)
             {
                 if (!_unsupportedTypes.Contains(type))
index 0ed6a3b..f13e481 100644 (file)
@@ -9,11 +9,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
 {
     internal sealed record KnownTypeSymbols
     {
-        public INamedTypeSymbol GenericIList { get; }
-        public INamedTypeSymbol ICollection { get; }
-        public INamedTypeSymbol IEnumerable { get; }
         public INamedTypeSymbol String { get; }
-
         public INamedTypeSymbol? CultureInfo { get; }
         public INamedTypeSymbol? DateOnly { get; }
         public INamedTypeSymbol? DateTimeOffset { get; }
@@ -26,17 +22,27 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
         public INamedTypeSymbol? Uri { get; }
         public INamedTypeSymbol? Version { get; }
 
-        public INamedTypeSymbol? Action { get; }
         public INamedTypeSymbol? ActionOfBinderOptions { get; }
-        public INamedTypeSymbol? BinderOptions { get; }
         public INamedTypeSymbol? ConfigurationKeyNameAttribute { get; }
+
+        public INamedTypeSymbol GenericIList_Unbound { get; }
+        public INamedTypeSymbol GenericICollection_Unbound { get; }
+        public INamedTypeSymbol GenericICollection { get; }
+        public INamedTypeSymbol GenericIEnumerable_Unbound { get; }
+        public INamedTypeSymbol IEnumerable { get; }
         public INamedTypeSymbol? Dictionary { get; }
+        public INamedTypeSymbol? GenericIDictionary_Unbound { get; }
         public INamedTypeSymbol? GenericIDictionary { get; }
         public INamedTypeSymbol? HashSet { get; }
         public INamedTypeSymbol? IConfiguration { get; }
         public INamedTypeSymbol? IConfigurationSection { get; }
         public INamedTypeSymbol? IDictionary { get; }
+        public INamedTypeSymbol? IReadOnlyCollection_Unbound { get; }
+        public INamedTypeSymbol? IReadOnlyDictionary_Unbound { get; }
+        public INamedTypeSymbol? IReadOnlyList_Unbound { get; }
+        public INamedTypeSymbol? IReadOnlySet_Unbound { get; }
         public INamedTypeSymbol? IServiceCollection { get; }
+        public INamedTypeSymbol? ISet_Unbound { get; }
         public INamedTypeSymbol? ISet { get; }
         public INamedTypeSymbol? List { get; }
 
@@ -56,29 +62,39 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
             Version = compilation.GetBestTypeByMetadataName(TypeFullName.Version);
 
             // Used to verify input configuation binding API calls.
-            Action = compilation.GetBestTypeByMetadataName(TypeFullName.Action);
-            BinderOptions = compilation.GetBestTypeByMetadataName(TypeFullName.BinderOptions);
-            ActionOfBinderOptions = Action?.Construct(BinderOptions);
+            INamedTypeSymbol? binderOptions = compilation.GetBestTypeByMetadataName(TypeFullName.BinderOptions);
+            ActionOfBinderOptions = binderOptions is null ? null : compilation.GetBestTypeByMetadataName(TypeFullName.Action)?.Construct(binderOptions);
 
             ConfigurationKeyNameAttribute = compilation.GetBestTypeByMetadataName(TypeFullName.ConfigurationKeyNameAttribute);
             IConfiguration = compilation.GetBestTypeByMetadataName(TypeFullName.IConfiguration);
             IConfigurationSection = compilation.GetBestTypeByMetadataName(TypeFullName.IConfigurationSection);
             IServiceCollection = compilation.GetBestTypeByMetadataName(TypeFullName.IServiceCollection);
 
-            // Collections.
+            // Used to test what kind of collection a type is.
             IEnumerable = compilation.GetSpecialType(SpecialType.System_Collections_IEnumerable);
             IDictionary = compilation.GetBestTypeByMetadataName(TypeFullName.IDictionary);
 
-            // Used for type equivalency checks for unbounded generics.
-            ICollection = compilation.GetSpecialType(SpecialType.System_Collections_Generic_ICollection_T).ConstructUnboundGenericType();
-            GenericIDictionary = compilation.GetBestTypeByMetadataName(TypeFullName.GenericIDictionary)?.ConstructUnboundGenericType();
-            GenericIList = compilation.GetSpecialType(SpecialType.System_Collections_Generic_IList_T).ConstructUnboundGenericType();
-            ISet = compilation.GetBestTypeByMetadataName(TypeFullName.ISet)?.ConstructUnboundGenericType();
-
-            // Used to construct concrete types at runtime; cannot also be constructed.
+            // Used to construct concrete type symbols for generic types, given their type parameters.
+            // These concrete types are used to generating instantiation and casting logic in the emitted binding code.
             Dictionary = compilation.GetBestTypeByMetadataName(TypeFullName.Dictionary);
+            GenericICollection = compilation.GetSpecialType(SpecialType.System_Collections_Generic_ICollection_T);
+            GenericIDictionary = compilation.GetBestTypeByMetadataName(TypeFullName.GenericIDictionary);
             HashSet = compilation.GetBestTypeByMetadataName(TypeFullName.HashSet);
             List = compilation.GetBestTypeByMetadataName(TypeFullName.List);
+            ISet = compilation.GetBestTypeByMetadataName(TypeFullName.ISet);
+
+            // Used for type equivalency checks for unbound generics. The parameters of the types
+            // retured by the Roslyn Get*Type* APIs are not unbound, so we construct unbound
+            // generics to equal those corresponding to generic types in the input type graphs.
+            GenericICollection_Unbound = GenericICollection?.ConstructUnboundGenericType();
+            GenericIDictionary_Unbound = GenericIDictionary?.ConstructUnboundGenericType();
+            GenericIEnumerable_Unbound = compilation.GetSpecialType(SpecialType.System_Collections_Generic_IEnumerable_T).ConstructUnboundGenericType();
+            GenericIList_Unbound = compilation.GetSpecialType(SpecialType.System_Collections_Generic_IList_T).ConstructUnboundGenericType();
+            IReadOnlyDictionary_Unbound = compilation.GetBestTypeByMetadataName(TypeFullName.IReadOnlyDictionary)?.ConstructUnboundGenericType();
+            IReadOnlyCollection_Unbound = compilation.GetBestTypeByMetadataName(TypeFullName.IReadOnlyCollection)?.ConstructUnboundGenericType();
+            IReadOnlyList_Unbound = compilation.GetBestTypeByMetadataName(TypeFullName.IReadOnlyList)?.ConstructUnboundGenericType();
+            IReadOnlySet_Unbound = compilation.GetBestTypeByMetadataName(TypeFullName.IReadOnlySet)?.ConstructUnboundGenericType();
+            ISet_Unbound = ISet?.ConstructUnboundGenericType();
         }
 
         private static class TypeFullName
@@ -98,6 +114,10 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
             public const string IConfigurationSection = "Microsoft.Extensions.Configuration.IConfigurationSection";
             public const string IDictionary = "System.Collections.Generic.IDictionary";
             public const string Int128 = "System.Int128";
+            public const string IReadOnlyCollection = "System.Collections.Generic.IReadOnlyCollection`1";
+            public const string IReadOnlyDictionary = "System.Collections.Generic.IReadOnlyDictionary`2";
+            public const string IReadOnlyList = "System.Collections.Generic.IReadOnlyList`1";
+            public const string IReadOnlySet = "System.Collections.Generic.IReadOnlySet`1";
             public const string ISet = "System.Collections.Generic.ISet`1";
             public const string IServiceCollection = "Microsoft.Extensions.DependencyInjection.IServiceCollection";
             public const string List = "System.Collections.Generic.List`1";
index f86f668..8b7b591 100644 (file)
@@ -19,14 +19,13 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
 
         public bool IsInterface { get; }
 
-        public CollectionSpec? ConcreteType { get; init; }
-    }
+        public CollectionSpec? ConcreteType { get; set; }
 
-    internal sealed record ArraySpec : CollectionSpec
-    {
-        public ArraySpec(ITypeSymbol type) : base(type) { }
+        public CollectionSpec? PopulationCastType { get; set; }
+
+        public required CollectionPopulationStrategy PopulationStrategy { get; init; }
 
-        public override TypeSpecKind SpecKind => TypeSpecKind.Array;
+        public required string? ToEnumerableMethodCall { get; init; }
     }
 
     internal sealed record EnumerableSpec : CollectionSpec
@@ -44,4 +43,12 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
 
         public required ParsableFromStringTypeSpec KeyType { get; init; }
     }
+
+    internal enum CollectionPopulationStrategy
+    {
+        Unknown,
+        Array,
+        Add,
+        Cast_Then_Add,
+    }
 }
index 21db025..d5c454b 100644 (file)
@@ -5,7 +5,9 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
 {
     internal enum ConstructionStrategy
     {
-        NotApplicable = 0,
+        None = 0,
         ParameterlessConstructor = 1,
+        ParameterizedConstructor = 2,
+        ToEnumerableMethod = 3,
     }
 }
index 6c0c430..95de8df 100644 (file)
@@ -10,6 +10,6 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
     {
         public ObjectSpec(INamedTypeSymbol type) : base(type) { }
         public override TypeSpecKind SpecKind => TypeSpecKind.Object;
-        public Dictionary<string, PropertySpec> Properties { get; } = new();
+        public Dictionary<string, PropertySpec?> Properties { get; } = new();
     }
 }
index 13a83fa..e9e384c 100644 (file)
@@ -1,13 +1,19 @@
 ï»¿// Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Linq;
 using Microsoft.CodeAnalysis;
 
 namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
 {
     internal sealed record PropertySpec
     {
+        public string Name { get; }
+        public bool IsStatic { get; }
+        public bool CanGet { get; }
+        public bool CanSet { get; }
+        public required TypeSpec? Type { get; init; }
+        public required string ConfigurationKeyName { get; init; }
+
         public PropertySpec(IPropertySymbol property)
         {
             Name = property.Name;
@@ -16,11 +22,9 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
             CanSet = property.SetMethod is IMethodSymbol { DeclaredAccessibility: Accessibility.Public, IsInitOnly: false };
         }
 
-        public string Name { get; }
-        public bool IsStatic { get; }
-        public bool CanGet { get; }
-        public bool CanSet { get; }
-        public required TypeSpec Type { get; init; }
-        public required string ConfigurationKeyName { get; init; }
+        public bool ShouldBind() =>
+            (CanGet || CanSet) &&
+            Type is not null &&
+            !(!CanSet && (Type as CollectionSpec)?.ConstructionStrategy is ConstructionStrategy.ParameterizedConstructor);
     }
 }
index 619a116..2acdc25 100644 (file)
@@ -640,7 +640,7 @@ namespace Microsoft.Extensions
             Assert.Equal("val_3", options.AlreadyInitializedHashSetDictionary["123"].ElementAt(3));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanOverrideExistingDictionaryKey()
         {
             var input = new Dictionary<string, string>
@@ -826,7 +826,7 @@ namespace Microsoft.Extensions
 #endif
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void GetStringArray()
         {
             var input = new Dictionary<string, string>
@@ -855,7 +855,7 @@ namespace Microsoft.Extensions
         }
 
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void BindStringArray()
         {
             var input = new Dictionary<string, string>
@@ -883,7 +883,7 @@ namespace Microsoft.Extensions
             Assert.Equal("valx", array[3]);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void GetAlreadyInitializedArray()
         {
             var input = new Dictionary<string, string>
@@ -913,7 +913,7 @@ namespace Microsoft.Extensions
             Assert.Equal("valx", array[6]);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void BindAlreadyInitializedArray()
         {
             var input = new Dictionary<string, string>
@@ -994,7 +994,7 @@ namespace Microsoft.Extensions
                 exception.Message);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void JaggedArrayBinding()
         {
             var input = new Dictionary<string, string>
@@ -1023,7 +1023,7 @@ namespace Microsoft.Extensions
             Assert.Equal("12", options.JaggedArray[1][2]);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void ReadOnlyArrayIsIgnored()
         {
             var input = new Dictionary<string, string>
@@ -1042,7 +1042,7 @@ namespace Microsoft.Extensions
             Assert.Equal(new OptionsWithArrays().ReadOnlyArray, options.ReadOnlyArray);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindUninitializedIEnumerable()
         {
             var input = new Dictionary<string, string>
@@ -1070,7 +1070,7 @@ namespace Microsoft.Extensions
             Assert.Equal("valx", array[3]);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInitializedIEnumerableAndTheOriginalItemsAreNotMutated()
         {
             var input = new Dictionary<string, string>
@@ -1117,7 +1117,7 @@ namespace Microsoft.Extensions
             Assert.Equal("ExtraItem", options.ICollectionNoSetter.ElementAt(2));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInitializedCustomIEnumerableBasedList()
         {
             // A field declared as IEnumerable<T> that is instantiated with a class
@@ -1147,7 +1147,7 @@ namespace Microsoft.Extensions
             Assert.Equal("val1", array[3]);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInitializedCustomIndirectlyDerivedIEnumerableList()
         {
             // A field declared as IEnumerable<T> that is instantiated with a class
@@ -1177,7 +1177,7 @@ namespace Microsoft.Extensions
             Assert.Equal("val1", array[3]);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInitializedIReadOnlyDictionaryAndDoesNotModifyTheOriginal()
         {
             // A field declared as IEnumerable<T> that is instantiated with a class
@@ -1242,7 +1242,7 @@ namespace Microsoft.Extensions
             Assert.Equal("ExtraItem", options.ICollection.ElementAt(4));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindUninitializedIList()
         {
             var input = new Dictionary<string, string>
@@ -1355,7 +1355,7 @@ namespace Microsoft.Extensions
             Assert.Equal("val_3", options.IDictionary["ghi"]);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindUninitializedIReadOnlyDictionary()
         {
             var input = new Dictionary<string, string>
@@ -1405,7 +1405,7 @@ namespace Microsoft.Extensions
         /// <summary>
         /// Replicates scenario from https://github.com/dotnet/runtime/issues/63479
         /// </summary>
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void TestCanBindListPropertyWithoutSetter()
         {
             var input = new Dictionary<string, string>
@@ -1424,7 +1424,7 @@ namespace Microsoft.Extensions
             Assert.Equal(new[] { "a", "b" }, options.ListPropertyWithoutSetter);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindNonInstantiatedIEnumerableWithItems()
         {
             var dic = new Dictionary<string, string>
@@ -1487,7 +1487,7 @@ namespace Microsoft.Extensions
         }
 
 #if NETCOREAPP
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInstantiatedIReadOnlySet()
         {
             var dic = new Dictionary<string, string>
@@ -1529,7 +1529,7 @@ namespace Microsoft.Extensions
             Assert.Equal("Yo2", options.InstantiatedIReadOnlySetWithSomeValues.ElementAt(3));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindNonInstantiatedIReadOnlySet()
         {
             var dic = new Dictionary<string, string>
@@ -1549,7 +1549,7 @@ namespace Microsoft.Extensions
             Assert.Equal("Yo2", options.NonInstantiatedIReadOnlySet.ElementAt(1));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInstantiatedDictionaryOfIReadOnlySetWithSomeExistingValues()
         {
             var dic = new Dictionary<string, string>
@@ -1577,7 +1577,7 @@ namespace Microsoft.Extensions
         }
 #endif
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInstantiatedReadOnlyDictionary2()
         {
             var dic = new Dictionary<string, string>
@@ -1601,7 +1601,7 @@ namespace Microsoft.Extensions
 
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void BindInstantiatedIReadOnlyDictionary_CreatesCopyOfOriginal()
         {
             var dic = new Dictionary<string, string>
@@ -1626,7 +1626,7 @@ namespace Microsoft.Extensions
             Assert.Equal(3, options.Dictionary["item3"]);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void BindNonInstantiatedIReadOnlyDictionary()
         {
             var dic = new Dictionary<string, string>
@@ -1647,7 +1647,7 @@ namespace Microsoft.Extensions
             Assert.Equal(2, options.Dictionary["item2"]);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void BindInstantiatedConcreteDictionary_OverwritesOriginal()
         {
             var dic = new Dictionary<string, string>
@@ -1671,7 +1671,7 @@ namespace Microsoft.Extensions
             Assert.Equal(3, options.Dictionary["item3"]);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInstantiatedReadOnlyDictionary()
         {
             var dic = new Dictionary<string, string>
@@ -1694,7 +1694,7 @@ namespace Microsoft.Extensions
             Assert.Equal(4, resultingDictionary["item4"]);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindNonInstantiatedReadOnlyDictionary()
         {
             var dic = new Dictionary<string, string>
@@ -1714,7 +1714,6 @@ namespace Microsoft.Extensions
             Assert.Equal(4, options.NonInstantiatedReadOnlyDictionary["item4"]);
         }
 
-
         [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
         public void CanBindNonInstantiatedDictionaryOfISet()
         {
@@ -1739,7 +1738,7 @@ namespace Microsoft.Extensions
             Assert.Equal("bar-2", options.NonInstantiatedDictionaryWithISet["bar"].ElementAt(1));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInstantiatedDictionaryOfISet()
         {
             var dic = new Dictionary<string, string>
@@ -1763,7 +1762,7 @@ namespace Microsoft.Extensions
             Assert.Equal("bar-2", options.InstantiatedDictionaryWithHashSet["bar"].ElementAt(1));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInstantiatedDictionaryOfISetWithSomeExistingValues()
         {
             var dic = new Dictionary<string, string>
@@ -1790,7 +1789,7 @@ namespace Microsoft.Extensions
             Assert.Equal("bar-2", options.InstantiatedDictionaryWithHashSetWithSomeValues["bar"].ElementAt(1));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Dropped members for binding: diagnostic warning issued instead.
         public void ThrowsForCustomIEnumerableCollection()
         {
             var configurationBuilder = new ConfigurationBuilder();
@@ -1807,7 +1806,7 @@ namespace Microsoft.Extensions
                 exception.Message);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Dropped members for binding: diagnostic warning issued instead.
         public void ThrowsForCustomICollection()
         {
             var configurationBuilder = new ConfigurationBuilder();
@@ -1824,7 +1823,7 @@ namespace Microsoft.Extensions
                 exception.Message);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Dropped members for binding: diagnostic warning issued instead.
         public void ThrowsForCustomDictionary()
         {
             var configurationBuilder = new ConfigurationBuilder();
@@ -1841,7 +1840,7 @@ namespace Microsoft.Extensions
                 exception.Message);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Dropped members for binding: diagnostic warning issued instead.
         public void ThrowsForCustomSet()
         {
             var configurationBuilder = new ConfigurationBuilder();
@@ -1858,7 +1857,7 @@ namespace Microsoft.Extensions
                 exception.Message);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInstantiatedISet()
         {
             var dic = new Dictionary<string, string>
@@ -1879,7 +1878,7 @@ namespace Microsoft.Extensions
             Assert.Equal("Yo2", options.InstantiatedISet.ElementAt(1));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInstantiatedISetWithSomeValues()
         {
             var dic = new Dictionary<string, string>
@@ -1901,7 +1900,7 @@ namespace Microsoft.Extensions
             Assert.Equal("Yo2", options.InstantiatedISetWithSomeValues.ElementAt(3));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInstantiatedHashSetWithSomeValues()
         {
             var dic = new Dictionary<string, string>
@@ -1923,7 +1922,7 @@ namespace Microsoft.Extensions
             Assert.Equal("Yo2", options.InstantiatedHashSetWithSomeValues.ElementAt(3));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindNonInstantiatedHashSet()
         {
             var dic = new Dictionary<string, string>
@@ -1943,7 +1942,7 @@ namespace Microsoft.Extensions
             Assert.Equal("Yo2", options.NonInstantiatedHashSet.ElementAt(1));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInstantiatedSortedSetWithSomeValues()
         {
             var dic = new Dictionary<string, string>
@@ -1965,7 +1964,7 @@ namespace Microsoft.Extensions
             Assert.Equal("Yo2", options.InstantiatedSortedSetWithSomeValues.ElementAt(3));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindNonInstantiatedSortedSetWithSomeValues()
         {
             var dic = new Dictionary<string, string>
@@ -1985,7 +1984,7 @@ namespace Microsoft.Extensions
             Assert.Equal("Yo2", options.NonInstantiatedSortedSetWithSomeValues.ElementAt(1));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure exception messages are in sync
         public void DoesNotBindInstantiatedISetWithUnsupportedKeys()
         {
             var dic = new Dictionary<string, string>
@@ -2003,7 +2002,7 @@ namespace Microsoft.Extensions
             Assert.Equal(0, options.HashSetWithUnsupportedKey.Count);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Ensure exception messages are in sync
         public void DoesNotBindUninstantiatedISetWithUnsupportedKeys()
         {
             var dic = new Dictionary<string, string>
@@ -2021,7 +2020,7 @@ namespace Microsoft.Extensions
             Assert.Null(options.UninstantiatedHashSetWithUnsupportedKey);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInstantiatedIEnumerableWithItems()
         {
             var dic = new Dictionary<string, string>
@@ -2041,7 +2040,7 @@ namespace Microsoft.Extensions
             Assert.Equal("Yo2", options.InstantiatedIEnumerable.ElementAt(1));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInstantiatedCustomICollectionWithoutAnAddMethodWithItems()
         {
             var dic = new Dictionary<string, string>
@@ -2061,7 +2060,7 @@ namespace Microsoft.Extensions
             Assert.Equal("Yo2", options.InstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(1));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindNonInstantiatedCustomICollectionWithoutAnAddMethodWithItems()
         {
             var dic = new Dictionary<string, string>
@@ -2081,7 +2080,7 @@ namespace Microsoft.Extensions
             Assert.Equal("Yo2", options.NonInstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(1));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInstantiatedICollectionWithItems()
         {
             var dic = new Dictionary<string, string>
@@ -2101,7 +2100,7 @@ namespace Microsoft.Extensions
             Assert.Equal("Yo2", options.InstantiatedICollection.ElementAt(1));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInstantiatedIReadOnlyCollectionWithItems()
         {
             var dic = new Dictionary<string, string>
@@ -2121,7 +2120,7 @@ namespace Microsoft.Extensions
             Assert.Equal("Yo2", options.InstantiatedIReadOnlyCollection.ElementAt(1));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void CanBindInstantiatedIEnumerableWithNullItems()
         {
             var dic = new Dictionary<string, string>
@@ -2142,7 +2141,7 @@ namespace Microsoft.Extensions
             Assert.Equal("Yo2", options.InstantiatedIEnumerable.ElementAt(1));
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void DifferentDictionaryBindingCasesTest()
         {
             var dic = new Dictionary<string, string>() { { "key", "value" } };
@@ -2153,60 +2152,15 @@ namespace Microsoft.Extensions
             Assert.Single(config.Get<Dictionary<string, string>>());
             Assert.Single(config.Get<IDictionary<string, string>>());
             Assert.Single(config.Get<ExtendedDictionary<string, string>>());
+            // The System.Reflection.AmbiguousMatchException scenario that
+            // this test validates is not applicable. Source generator will
+            // statically bind to best-fit dictionary value indexer.
+#if !BUILDING_SOURCE_GENERATOR_TESTS
             Assert.Single(config.Get<ImplementerOfIDictionaryClass<string, string>>());
-        }
-
-        public class OptionsWithDifferentCollectionInterfaces
-        {
-            private static IEnumerable<string> s_instantiatedIEnumerable = new List<string> { "value1", "value2" };
-            public bool IsSameInstantiatedIEnumerable() => object.ReferenceEquals(s_instantiatedIEnumerable, InstantiatedIEnumerable);
-            public IEnumerable<string> InstantiatedIEnumerable { get; set; } = s_instantiatedIEnumerable;
-
-            private static IList<string> s_instantiatedIList = new List<string> { "value1", "value2" };
-            public bool IsSameInstantiatedIList() => object.ReferenceEquals(s_instantiatedIList, InstantiatedIList);
-            public IList<string> InstantiatedIList { get; set; } = s_instantiatedIList;
-
-            private static IReadOnlyList<string> s_instantiatedIReadOnlyList = new List<string> { "value1", "value2" };
-            public bool IsSameInstantiatedIReadOnlyList() => object.ReferenceEquals(s_instantiatedIReadOnlyList, InstantiatedIReadOnlyList);
-            public IReadOnlyList<string> InstantiatedIReadOnlyList { get; set; } = s_instantiatedIReadOnlyList;
-
-            private static IDictionary<string, string> s_instantiatedIDictionary = new Dictionary<string, string> { ["Key1"] = "value1", ["Key2"] = "value2" };
-            public IDictionary<string, string> InstantiatedIDictionary { get; set; } = s_instantiatedIDictionary;
-            public bool IsSameInstantiatedIDictionary() => object.ReferenceEquals(s_instantiatedIDictionary, InstantiatedIDictionary);
-
-            private static IReadOnlyDictionary<string, string> s_instantiatedIReadOnlyDictionary = new Dictionary<string, string> { ["Key1"] = "value1", ["Key2"] = "value2" };
-            public IReadOnlyDictionary<string, string> InstantiatedIReadOnlyDictionary { get; set; } = s_instantiatedIReadOnlyDictionary;
-            public bool IsSameInstantiatedIReadOnlyDictionary() => object.ReferenceEquals(s_instantiatedIReadOnlyDictionary, InstantiatedIReadOnlyDictionary);
-
-            private static ISet<string> s_instantiatedISet = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "a", "A", "b" };
-            public ISet<string> InstantiatedISet { get; set; } = s_instantiatedISet;
-            public bool IsSameInstantiatedISet() => object.ReferenceEquals(s_instantiatedISet, InstantiatedISet);
-
-#if NETCOREAPP
-            private static IReadOnlySet<string> s_instantiatedIReadOnlySet = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "a", "A", "b" };
-            public IReadOnlySet<string> InstantiatedIReadOnlySet { get; set; } = s_instantiatedIReadOnlySet;
-            public bool IsSameInstantiatedIReadOnlySet() => object.ReferenceEquals(s_instantiatedIReadOnlySet, InstantiatedIReadOnlySet);
-
-            public IReadOnlySet<string> UnInstantiatedIReadOnlySet { get; set; }
 #endif
-            private static ICollection<string> s_instantiatedICollection = new List<string> { "a", "b", "c" };
-            public ICollection<string> InstantiatedICollection { get; set; } = s_instantiatedICollection;
-            public bool IsSameInstantiatedICollection() => object.ReferenceEquals(s_instantiatedICollection, InstantiatedICollection);
-
-            private static IReadOnlyCollection<string> s_instantiatedIReadOnlyCollection = new List<string> { "a", "b", "c" };
-            public IReadOnlyCollection<string> InstantiatedIReadOnlyCollection { get; set; } = s_instantiatedIReadOnlyCollection;
-            public bool IsSameInstantiatedIReadOnlyCollection() => object.ReferenceEquals(s_instantiatedIReadOnlyCollection, InstantiatedIReadOnlyCollection);
-
-            public IReadOnlyCollection<string> UnInstantiatedIReadOnlyCollection { get; set; }
-            public ICollection<string> UnInstantiatedICollection { get; set; }
-            public ISet<string> UnInstantiatedISet { get; set; }
-            public IReadOnlyDictionary<string, string> UnInstantiatedIReadOnlyDictionary { get; set; }
-            public IEnumerable<string> UnInstantiatedIEnumerable { get; set; }
-            public IList<string> UnInstantiatedIList { get; set; }
-            public IReadOnlyList<string> UnInstantiatedIReadOnlyList { get; set; }
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void TestOptionsWithDifferentCollectionInterfaces()
         {
             var input = new Dictionary<string, string>
@@ -2314,7 +2268,7 @@ namespace Microsoft.Extensions
             Assert.Equal(new string[] { "r", "e" }, options.UnInstantiatedIReadOnlyCollection);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
+        [Fact]
         public void TestMutatingDictionaryValues()
         {
             IConfiguration config = new ConfigurationBuilder()
index 9242ae9..49d97a7 100644 (file)
@@ -94,10 +94,6 @@ namespace Microsoft.Extensions
             IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
         }
 
-        public class CustomDictionary<T> : Dictionary<string, T>
-        {
-        }
-
         public class NestedOptions
         {
             public int Integer { get; set; }
@@ -331,5 +327,55 @@ namespace Microsoft.Extensions
         public interface ICustomDictionary<T, T1> : IDictionary<T, T1>
         {
         }
+
+        public class OptionsWithDifferentCollectionInterfaces
+        {
+            private static IEnumerable<string> s_instantiatedIEnumerable = new List<string> { "value1", "value2" };
+            public bool IsSameInstantiatedIEnumerable() => object.ReferenceEquals(s_instantiatedIEnumerable, InstantiatedIEnumerable);
+            public IEnumerable<string> InstantiatedIEnumerable { get; set; } = s_instantiatedIEnumerable;
+
+            private static IList<string> s_instantiatedIList = new List<string> { "value1", "value2" };
+            public bool IsSameInstantiatedIList() => object.ReferenceEquals(s_instantiatedIList, InstantiatedIList);
+            public IList<string> InstantiatedIList { get; set; } = s_instantiatedIList;
+
+            private static IReadOnlyList<string> s_instantiatedIReadOnlyList = new List<string> { "value1", "value2" };
+            public bool IsSameInstantiatedIReadOnlyList() => object.ReferenceEquals(s_instantiatedIReadOnlyList, InstantiatedIReadOnlyList);
+            public IReadOnlyList<string> InstantiatedIReadOnlyList { get; set; } = s_instantiatedIReadOnlyList;
+
+            private static IDictionary<string, string> s_instantiatedIDictionary = new Dictionary<string, string> { ["Key1"] = "value1", ["Key2"] = "value2" };
+            public IDictionary<string, string> InstantiatedIDictionary { get; set; } = s_instantiatedIDictionary;
+            public bool IsSameInstantiatedIDictionary() => object.ReferenceEquals(s_instantiatedIDictionary, InstantiatedIDictionary);
+
+            private static IReadOnlyDictionary<string, string> s_instantiatedIReadOnlyDictionary = new Dictionary<string, string> { ["Key1"] = "value1", ["Key2"] = "value2" };
+            public IReadOnlyDictionary<string, string> InstantiatedIReadOnlyDictionary { get; set; } = s_instantiatedIReadOnlyDictionary;
+            public bool IsSameInstantiatedIReadOnlyDictionary() => object.ReferenceEquals(s_instantiatedIReadOnlyDictionary, InstantiatedIReadOnlyDictionary);
+
+            private static ISet<string> s_instantiatedISet = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "a", "A", "b" };
+            public ISet<string> InstantiatedISet { get; set; } = s_instantiatedISet;
+            public bool IsSameInstantiatedISet() => object.ReferenceEquals(s_instantiatedISet, InstantiatedISet);
+
+#if NETCOREAPP
+            private static IReadOnlySet<string> s_instantiatedIReadOnlySet = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "a", "A", "b" };
+            public IReadOnlySet<string> InstantiatedIReadOnlySet { get; set; } = s_instantiatedIReadOnlySet;
+            public bool IsSameInstantiatedIReadOnlySet() => object.ReferenceEquals(s_instantiatedIReadOnlySet, InstantiatedIReadOnlySet);
+
+            public IReadOnlySet<string> UnInstantiatedIReadOnlySet { get; set; }
+#endif
+            private static ICollection<string> s_instantiatedICollection = new List<string> { "a", "b", "c" };
+            public ICollection<string> InstantiatedICollection { get; set; } = s_instantiatedICollection;
+            public bool IsSameInstantiatedICollection() => object.ReferenceEquals(s_instantiatedICollection, InstantiatedICollection);
+
+            private static IReadOnlyCollection<string> s_instantiatedIReadOnlyCollection = new List<string> { "a", "b", "c" };
+            public IReadOnlyCollection<string> InstantiatedIReadOnlyCollection { get; set; } = s_instantiatedIReadOnlyCollection;
+            public bool IsSameInstantiatedIReadOnlyCollection() => object.ReferenceEquals(s_instantiatedIReadOnlyCollection, InstantiatedIReadOnlyCollection);
+
+            public IReadOnlyCollection<string> UnInstantiatedIReadOnlyCollection { get; set; }
+            public ICollection<string> UnInstantiatedICollection { get; set; }
+            public ISet<string> UnInstantiatedISet { get; set; }
+            public IReadOnlyDictionary<string, string> UnInstantiatedIReadOnlyDictionary { get; set; }
+            public IEnumerable<string> UnInstantiatedIEnumerable { get; set; }
+            public IList<string> UnInstantiatedIList { get; set; }
+            public IReadOnlyList<string> UnInstantiatedIReadOnlyList { get; set; }
+        }
     }
 }
index c025e19..121d9a9 100644 (file)
@@ -941,8 +941,8 @@ namespace Microsoft.Extensions
                 exception.Message);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
-        public void ExceptionWhenTryingToBindToConstructorWithMissingConfig() // Need support for parameterized ctors.
+        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors.
+        public void ExceptionWhenTryingToBindToConstructorWithMissingConfig()
         {
             var input = new Dictionary<string, string>
             {
@@ -961,8 +961,8 @@ namespace Microsoft.Extensions
                 exception.Message);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
-        public void ExceptionWhenTryingToBindConfigToClassWhereNoMatchingParameterIsFoundInConstructor() // Need support for parameterized ctors.
+        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors.
+        public void ExceptionWhenTryingToBindConfigToClassWhereNoMatchingParameterIsFoundInConstructor()
         {
             var input = new Dictionary<string, string>
             {
@@ -982,8 +982,8 @@ namespace Microsoft.Extensions
                 exception.Message);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))]
-        public void BindsToClassConstructorParametersWithDefaultValues() // Need support for parameterized ctors.
+        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need support for parameterized ctors.
+        public void BindsToClassConstructorParametersWithDefaultValues()
         {
             var input = new Dictionary<string, string>
             {
@@ -1520,7 +1520,7 @@ namespace Microsoft.Extensions
             Assert.True(bound.NullableNestedStruct.Value.DeeplyNested.Boolean);
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need collection support.
+        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need property selection in sync with reflection.
         public void CanBindVirtualProperties()
         {
             ConfigurationBuilder configurationBuilder = new();
@@ -1592,7 +1592,7 @@ namespace Microsoft.Extensions
 #endif
         }
 
-        [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need collection support.
+        [Fact]
         public void EnsureCallingThePropertySetter()
         {
             var json = @"{
@@ -1616,7 +1616,13 @@ namespace Microsoft.Extensions
             Assert.Equal(2, options.ParsedBlacklist.Count); // should be initialized when calling the options.Blacklist setter.
 
             Assert.Equal(401, options.HttpStatusCode); // exists in configuration and properly sets the property
-            Assert.Equal(2, options.OtherCode); // doesn't exist in configuration. the setter sets default value '2'
+#if BUILDING_SOURCE_GENERATOR_TESTS
+            // Setter not called if there's no matching configuration value.
+            Assert.Equal(0, options.OtherCode);
+#else
+            // doesn't exist in configuration. the setter sets default value '2'
+            Assert.Equal(2, options.OtherCode);
+#endif
         }
 
         [Fact]
diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestCollectionsGen.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestCollectionsGen.generated.txt
new file mode 100644 (file)
index 0000000..cba992e
--- /dev/null
@@ -0,0 +1,240 @@
+// <auto-generated/>
+#nullable enable
+
+internal static class GeneratedConfigurationBinder
+{
+    public static T? Get<T>(this global::Microsoft.Extensions.Configuration.IConfiguration configuration) => (T?)(global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.Helpers.GetCore(configuration, typeof(T), configureActions: null) ?? default(T));
+}
+
+namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
+{
+    using System;
+    using System.Globalization;
+    using Microsoft.Extensions.Configuration;
+    using System.Collections.Generic;
+    using System.Linq;
+
+    internal static class Helpers
+    {
+        public static object? GetCore(this IConfiguration configuration, Type type, Action<BinderOptions>? configureActions)
+        {
+            if (configuration is null)
+            {
+                throw new ArgumentNullException(nameof(configuration));
+            }
+
+            BinderOptions? binderOptions = GetBinderOptions(configureActions);
+
+            if (!HasValueOrChildren(configuration))
+            {
+                return null;
+            }
+
+            if (type == typeof(Program.MyClassWithCustomCollections))
+            {
+                var obj = new Program.MyClassWithCustomCollections();
+                BindCore(configuration, ref obj, binderOptions);
+                return obj;
+            }
+
+            throw new global::System.NotSupportedException($"Unable to bind to type '{type}': generator did not detect the type as input.");
+        }
+
+        public static void BindCore(IConfiguration configuration, ref Program.CustomDictionary<string, int> obj, BinderOptions? binderOptions)
+        {
+            if (obj is null)
+            {
+                throw new ArgumentNullException(nameof(obj));
+            }
+
+            foreach (IConfigurationSection section in configuration.GetChildren())
+            {
+                string key = section.Key;
+                int element;
+                if (section.Value is string stringValue1)
+                {
+                    element = ParseInt(stringValue1, () => section.Path);
+                    obj[key] = element;
+                }
+            }
+        }
+
+        public static void BindCore(IConfiguration configuration, ref Program.CustomList obj, BinderOptions? binderOptions)
+        {
+            if (obj is null)
+            {
+                throw new ArgumentNullException(nameof(obj));
+            }
+
+            foreach (IConfigurationSection section in configuration.GetChildren())
+            {
+                if (section.Value is string stringValue2)
+                {
+                    obj.Add(stringValue2);
+                }
+            }
+        }
+
+        public static void BindCore(IConfiguration configuration, ref IReadOnlyList<int> obj, BinderOptions? binderOptions)
+        {
+            if (obj is null)
+            {
+                throw new ArgumentNullException(nameof(obj));
+            }
+
+            if (obj is not ICollection<int> temp)
+            {
+                return;
+            }
+
+            foreach (IConfigurationSection section in configuration.GetChildren())
+            {
+                int element;
+                if (section.Value is string stringValue3)
+                {
+                    element = ParseInt(stringValue3, () => section.Path);
+                    temp.Add(element);
+                }
+            }
+        }
+
+        public static void BindCore(IConfiguration configuration, ref IReadOnlyDictionary<string, int> obj, BinderOptions? binderOptions)
+        {
+            if (obj is null)
+            {
+                throw new ArgumentNullException(nameof(obj));
+            }
+
+            if (obj is not IDictionary<string, int> temp)
+            {
+                return;
+            }
+
+            foreach (IConfigurationSection section in configuration.GetChildren())
+            {
+                string key = section.Key;
+                int element;
+                if (section.Value is string stringValue4)
+                {
+                    element = ParseInt(stringValue4, () => section.Path);
+                    temp[key] = element;
+                }
+            }
+        }
+
+        public static void BindCore(IConfiguration configuration, ref Program.MyClassWithCustomCollections obj, BinderOptions? binderOptions)
+        {
+            if (obj is null)
+            {
+                throw new ArgumentNullException(nameof(obj));
+            }
+
+            List<string>? temp = null;
+            foreach (IConfigurationSection section in configuration.GetChildren())
+            {
+                switch (section.Key)
+                {
+                    case "CustomDictionary":
+                    {
+                        if (HasChildren(section))
+                        {
+                            Program.CustomDictionary<string, int> temp5 = obj.CustomDictionary;
+                            temp5 ??= new Program.CustomDictionary<string, int>();
+                            BindCore(section, ref temp5, binderOptions);
+                            obj.CustomDictionary = temp5;
+                        }
+                    }
+                    break;
+                    case "CustomList":
+                    {
+                        if (HasChildren(section))
+                        {
+                            Program.CustomList temp6 = obj.CustomList;
+                            temp6 ??= new Program.CustomList();
+                            BindCore(section, ref temp6, binderOptions);
+                            obj.CustomList = temp6;
+                        }
+                    }
+                    break;
+                    case "ICustomDictionary":
+                    {
+                    }
+                    break;
+                    case "ICustomCollection":
+                    {
+                    }
+                    break;
+                    case "IReadOnlyList":
+                    {
+                        if (HasChildren(section))
+                        {
+                            IReadOnlyList<int> temp7 = obj.IReadOnlyList;
+                            temp7 = temp7 is null ? new List<int>() : new List<int>(temp7);
+                            BindCore(section, ref temp7, binderOptions);
+                            obj.IReadOnlyList = temp7;
+                        }
+                    }
+                    break;
+                    case "UnsupportedIReadOnlyDictionaryUnsupported":
+                    {
+                    }
+                    break;
+                    case "IReadOnlyDictionary":
+                    {
+                        if (HasChildren(section))
+                        {
+                            IReadOnlyDictionary<string, int> temp8 = obj.IReadOnlyDictionary;
+                            temp8 = temp8 is null ? new Dictionary<string, int>() : temp8.ToDictionary(pair => pair.Key, pair => pair.Value);
+                            BindCore(section, ref temp8, binderOptions);
+                            obj.IReadOnlyDictionary = temp8;
+                        }
+                    }
+                    break;
+                    default:
+                    {
+                        if (binderOptions?.ErrorOnUnknownConfiguration == true)
+                        {
+                            (temp ??= new List<string>()).Add($"'{section.Key}'");
+                        }
+                    }
+                    break;
+                }
+            }
+
+            if (temp is not null)
+            {
+                throw new InvalidOperationException($"'ErrorOnUnknownConfiguration' was set on the provided BinderOptions, but the following properties were not found on the instance of {typeof(Program.MyClassWithCustomCollections)}: {string.Join(", ", temp)}");
+            }
+        }
+
+        public static bool HasValueOrChildren(IConfiguration configuration)
+        {
+            if ((configuration as IConfigurationSection)?.Value is not null)
+            {
+                return true;
+            }
+            return HasChildren(configuration);
+        }
+
+        public static bool HasChildren(IConfiguration configuration)
+        {
+            foreach (IConfigurationSection section in configuration.GetChildren())
+            {
+                return true;
+            }
+            return false;
+        }
+
+        public static int ParseInt(string stringValue, Func<string?> getPath)
+        {
+            try
+            {
+                return int.Parse(stringValue, NumberStyles.Integer, CultureInfo.InvariantCulture);
+            }
+            catch (Exception exception)
+            {
+                throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(int)}'.", exception);
+            }
+        }
+    }
+}
index 6359acb..cbc6b50 100644 (file)
@@ -3,9 +3,11 @@
 
 using System;
 using System.Collections;
+using System.Collections.Generic;
 using System.Collections.Immutable;
 using System.Globalization;
 using System.IO;
+using System.Linq;
 using System.Threading.Tasks;
 using Microsoft.CodeAnalysis;
 using Microsoft.CodeAnalysis.CSharp;
@@ -37,7 +39,7 @@ public class Program
         config.Bind(configObj, options => { })
         config.Bind(""key"", configObj);
        }
-       
+
        public class MyClass
        {
                public string MyString { get; set; }
@@ -75,7 +77,7 @@ public class Program
         configObj = config.Get<MyClass>(binderOptions => { });
         configObj = config.Get(typeof(MyClass2), binderOptions => { });
        }
-       
+
        public class MyClass
        {
                public string MyString { get; set; }
@@ -159,7 +161,7 @@ public class Program
                ServiceCollection services = new();
         services.Configure<MyClass>(section);
        }
-       
+
        public class MyClass
        {
                public string MyString { get; set; }
@@ -249,19 +251,84 @@ public class Program
             Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity);
         }
 
+        [Fact]
+        public async Task TestCollectionsGen()
+        {
+            string testSourceCode = """
+                using System.Collections.Generic;
+                using Microsoft.Extensions.Configuration;
+
+                public class Program
+                {
+                    public static void Main()
+                    {
+                        ConfigurationBuilder configurationBuilder = new();
+                        IConfiguration config = configurationBuilder.Build();
+                        IConfigurationSection section = config.GetSection(""MySection"");
+
+                        section.Get<MyClassWithCustomCollections>();
+                    }
+
+                    public class MyClassWithCustomCollections
+                    {
+                        public CustomDictionary<string, int> CustomDictionary { get; set; }
+                        public CustomList CustomList { get; set; }
+                        public ICustomDictionary<string> ICustomDictionary { get; set; }
+                        public ICustomSet<MyClassWithCustomCollections> ICustomCollection { get; set; }
+                        public IReadOnlyList<int> IReadOnlyList { get; set; }
+                        public IReadOnlyDictionary<MyClassWithCustomCollections, int> UnsupportedIReadOnlyDictionaryUnsupported { get; set; }
+                        public IReadOnlyDictionary<string, int> IReadOnlyDictionary { get; set; }
+                    }
+
+                    public class CustomDictionary<TKey, TValue> : Dictionary<TKey, TValue>
+                    {
+                    }
+
+                    public class CustomList : List<string>
+                    {
+                    }
+
+                    public interface ICustomDictionary<T> : IDictionary<T, string>
+                    {
+                    }
+
+                    public interface ICustomSet<T> : ISet<T>
+                    {
+                    }
+                }
+                """;
+
+            await VerifyAgainstBaselineUsingFile("TestCollectionsGen.generated.txt", testSourceCode, assessDiagnostics: (d) =>
+            {
+                Assert.Equal(6, d.Length);
+                Test(d.Where(diagnostic => diagnostic.Id is "SYSLIB1100"), "Did not generate binding logic for a type");
+                Test(d.Where(diagnostic => diagnostic.Id is "SYSLIB1101"), "Did not generate binding logic for a property on a type");
+
+                static void Test(IEnumerable<Diagnostic> d, string expectedTitle)
+                {
+                    Assert.Equal(3, d.Count());
+                    foreach (Diagnostic diagnostic in d)
+                    {
+                        Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
+                        Assert.Contains(expectedTitle, diagnostic.Descriptor.Title.ToString(CultureInfo.InvariantCulture));
+                    }
+                }
+            });
+        }
+
         private async Task VerifyAgainstBaselineUsingFile(
             string filename,
             string testSourceCode,
-            LanguageVersion languageVersion = LanguageVersion.Preview)
+            LanguageVersion languageVersion = LanguageVersion.Preview,
+            Action<ImmutableArray<Diagnostic>>? assessDiagnostics = null)
         {
             string baseline = LineEndingsHelper.Normalize(await File.ReadAllTextAsync(Path.Combine("Baselines", filename)).ConfigureAwait(false));
             string[] expectedLines = baseline.Replace("%VERSION%", typeof(ConfigurationBindingSourceGenerator).Assembly.GetName().Version?.ToString())
                                              .Split(Environment.NewLine);
 
             var (d, r) = await RunGenerator(testSourceCode, languageVersion);
-
-            Assert.Empty(d);
             Assert.Single(r);
+            (assessDiagnostics ?? ((d) => Assert.Empty(d))).Invoke(d);
 
             Assert.True(RoslynTestUtils.CompareLines(expectedLines, r[0].SourceText,
                 out string errorMessage), errorMessage);