diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs index d68c5c8b1fcc71..33b5295b5078c6 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Emitter.cs @@ -606,11 +606,6 @@ private void EmitBindCoreImpl(TypeSpec type) { switch (type.SpecKind) { - case TypeSpecKind.Array: - { - EmitBindCoreImplForArray((ArraySpec)type); - } - break; case TypeSpecKind.Enumerable: { EmitBindCoreImplForEnumerable((EnumerableSpec)type); @@ -643,11 +638,23 @@ private void EmitBindCoreImpl(TypeSpec type) } } - 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 @@ private void EmitBindCoreImplForArray(ArraySpec type) """); } - 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 @@ private void EmitBindCoreImplForEnumerable(EnumerableSpec type) { 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 @@ private void EmitBindCoreImplForDictionary(DictionarySpec type) { 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 @@ private void EmitBindCoreImplForDictionary(DictionarySpec type) void Emit_BindAndAddLogic_ForElement() { - TypeSpec elementType = type.ElementType; - if (elementType.SpecKind == TypeSpecKind.ParsableFromString) { ParsableFromStringTypeSpec stringParsableType = (ParsableFromStringTypeSpec)elementType; @@ -732,7 +740,7 @@ void Emit_BindAndAddLogic_ForElement() { 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 @@ void Emit_BindAndAddLogic_ForElement() 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 @@ private void EmitBindCoreImplForObject(ObjectSpec type) { _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 @@ private void EmitBindCoreImplForProperty(PropertySpec property, TypeSpec propert break; default: { - EmitBindCoreCallForProperty( - property, - propertyType, - expressionForPropertyAccess); + EmitBindCoreCallForProperty(property, propertyType, expressionForPropertyAccess); } break; } @@ -1034,25 +1066,33 @@ private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, Ini 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 @@ private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, Ini } 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 @@ private void EmitObjectInit(TypeSpec type, string expressionForMemberAccess, Ini } } + 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; diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs index b8d5dec48c7ad1..c92bbc03009401 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Helpers.cs @@ -11,7 +11,6 @@ public sealed partial class ConfigurationBindingSourceGenerator 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)); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs index 45d71cd60d9820..e44c77651536ab 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingSourceGenerator.Parser.cs @@ -7,6 +7,7 @@ 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 @@ private void ProcessConfigureCall(BinderInvocationOperation binderOperation) 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 @@ void RegisterBindCoreGenType(TypeSpec? spec) { if (spec is not null) { - GetRootConfigTypeCache(MethodSpecifier.BindCore).Add(spec); + AddToRootConfigTypeCache(MethodSpecifier.BindCore, spec); _methodsToGen |= MethodSpecifier.BindCore; } } } - private HashSet GetRootConfigTypeCache(MethodSpecifier method) + private void AddToRootConfigTypeCache(MethodSpecifier method, TypeSpec spec) { + Debug.Assert(spec is not null); + if (!_rootConfigTypes.TryGetValue(method, out HashSet types)) { _rootConfigTypes[method] = types = new HashSet(); } - return types; + types.Add(spec); } private static bool IsNullable(ITypeSymbol type, [NotNullWhen(true)] out ITypeSymbol? underlyingType) @@ -542,7 +545,7 @@ private bool TryGetTypeSpec(ITypeSymbol type, DiagnosticDescriptor descriptor, o { spec = GetOrCreateTypeSpec(type); - if (spec == null) + if (spec is null) { ReportUnsupportedType(type, descriptor); return false; @@ -551,7 +554,7 @@ private bool TryGetTypeSpec(ITypeSymbol type, DiagnosticDescriptor descriptor, o 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 @@ private bool TryGetTypeSpec(ITypeSymbol type, DiagnosticDescriptor descriptor, o } // We want a BindCore method for List 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 @@ private bool IsSupportedArrayType(ITypeSymbol type, Location? location) { 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 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc 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 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc 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 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc 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 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc { 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 @@ private DictionarySpec CreateDictionarySpec(INamedTypeSymbol type, Location? loc } 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 @@ TypeSpecKind.Enumerable or } } - 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 @@ private bool IsCandidateEnumerable(INamedTypeSymbol type, out ITypeSymbol? eleme 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 @@ public static bool ContainsGenericParameters(INamedTypeSymbol type) 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 @@ private static bool HasAddMethod(INamedTypeSymbol type, ITypeSymbol element) 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 @@ private static bool HasAddMethod(INamedTypeSymbol type, ITypeSymbol element, ITy 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)) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/KnownTypeSymbols.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/KnownTypeSymbols.cs index 0ed6a3ba3ca24f..f13e481b4982f4 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/KnownTypeSymbols.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/KnownTypeSymbols.cs @@ -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 @@ internal sealed record KnownTypeSymbols 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 @@ public KnownTypeSymbols(CSharpCompilation compilation) 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 @@ private static class TypeFullName 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"; diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/CollectionSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/CollectionSpec.cs index f86f66803be639..8b7b591524af5b 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/CollectionSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/CollectionSpec.cs @@ -19,14 +19,13 @@ public CollectionSpec(ITypeSymbol type) : base(type) 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 @@ public DictionarySpec(INamedTypeSymbol type) : base(type) { } public required ParsableFromStringTypeSpec KeyType { get; init; } } + + internal enum CollectionPopulationStrategy + { + Unknown, + Array, + Add, + Cast_Then_Add, + } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/ConstructionStrategy.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/ConstructionStrategy.cs index 21db02547258ac..d5c454bcecb5ec 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/ConstructionStrategy.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/ConstructionStrategy.cs @@ -5,7 +5,9 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { internal enum ConstructionStrategy { - NotApplicable = 0, + None = 0, ParameterlessConstructor = 1, + ParameterizedConstructor = 2, + ToEnumerableMethod = 3, } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/ObjectSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/ObjectSpec.cs index 6c0c43073d9c6c..95de8dfd72f67a 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/ObjectSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/ObjectSpec.cs @@ -10,6 +10,6 @@ internal sealed record ObjectSpec : TypeSpec { public ObjectSpec(INamedTypeSymbol type) : base(type) { } public override TypeSpecKind SpecKind => TypeSpecKind.Object; - public Dictionary Properties { get; } = new(); + public Dictionary Properties { get; } = new(); } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/PropertySpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/PropertySpec.cs index 13a83fadf73249..e9e384cd466cdd 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/PropertySpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/TypeGraph/PropertySpec.cs @@ -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 @@ public PropertySpec(IPropertySymbol property) 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); } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Collections.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Collections.cs index 619a11627683d5..2acdc25046cc32 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Collections.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Collections.cs @@ -640,7 +640,7 @@ public void AlreadyInitializedHashSetDictionaryBinding() Assert.Equal("val_3", options.AlreadyInitializedHashSetDictionary["123"].ElementAt(3)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanOverrideExistingDictionaryKey() { var input = new Dictionary @@ -826,7 +826,7 @@ public void NonStringKeyDictionaryBinding() #endif } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void GetStringArray() { var input = new Dictionary @@ -855,7 +855,7 @@ public void GetStringArray() } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void BindStringArray() { var input = new Dictionary @@ -883,7 +883,7 @@ public void BindStringArray() Assert.Equal("valx", array[3]); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void GetAlreadyInitializedArray() { var input = new Dictionary @@ -913,7 +913,7 @@ public void GetAlreadyInitializedArray() Assert.Equal("valx", array[6]); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void BindAlreadyInitializedArray() { var input = new Dictionary @@ -994,7 +994,7 @@ public void UnsupportedMultidimensionalArrays() exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void JaggedArrayBinding() { var input = new Dictionary @@ -1023,7 +1023,7 @@ public void JaggedArrayBinding() Assert.Equal("12", options.JaggedArray[1][2]); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void ReadOnlyArrayIsIgnored() { var input = new Dictionary @@ -1042,7 +1042,7 @@ public void ReadOnlyArrayIsIgnored() Assert.Equal(new OptionsWithArrays().ReadOnlyArray, options.ReadOnlyArray); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindUninitializedIEnumerable() { var input = new Dictionary @@ -1070,7 +1070,7 @@ public void CanBindUninitializedIEnumerable() Assert.Equal("valx", array[3]); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInitializedIEnumerableAndTheOriginalItemsAreNotMutated() { var input = new Dictionary @@ -1117,7 +1117,7 @@ public void CanBindInitializedIEnumerableAndTheOriginalItemsAreNotMutated() Assert.Equal("ExtraItem", options.ICollectionNoSetter.ElementAt(2)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInitializedCustomIEnumerableBasedList() { // A field declared as IEnumerable that is instantiated with a class @@ -1147,7 +1147,7 @@ public void CanBindInitializedCustomIEnumerableBasedList() Assert.Equal("val1", array[3]); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInitializedCustomIndirectlyDerivedIEnumerableList() { // A field declared as IEnumerable that is instantiated with a class @@ -1177,7 +1177,7 @@ public void CanBindInitializedCustomIndirectlyDerivedIEnumerableList() Assert.Equal("val1", array[3]); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInitializedIReadOnlyDictionaryAndDoesNotModifyTheOriginal() { // A field declared as IEnumerable that is instantiated with a class @@ -1242,7 +1242,7 @@ public void CanBindUninitializedICollection() Assert.Equal("ExtraItem", options.ICollection.ElementAt(4)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindUninitializedIList() { var input = new Dictionary @@ -1355,7 +1355,7 @@ public void CanBindUninitializedIDictionary() Assert.Equal("val_3", options.IDictionary["ghi"]); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindUninitializedIReadOnlyDictionary() { var input = new Dictionary @@ -1405,7 +1405,7 @@ public void CanBindWithInterdependentProperties() /// /// Replicates scenario from https://github.com/dotnet/runtime/issues/63479 /// - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void TestCanBindListPropertyWithoutSetter() { var input = new Dictionary @@ -1424,7 +1424,7 @@ public void TestCanBindListPropertyWithoutSetter() Assert.Equal(new[] { "a", "b" }, options.ListPropertyWithoutSetter); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindNonInstantiatedIEnumerableWithItems() { var dic = new Dictionary @@ -1487,7 +1487,7 @@ public void CanBindISetNoSetter() } #if NETCOREAPP - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInstantiatedIReadOnlySet() { var dic = new Dictionary @@ -1529,7 +1529,7 @@ public void CanBindInstantiatedIReadOnlyWithSomeValues() Assert.Equal("Yo2", options.InstantiatedIReadOnlySetWithSomeValues.ElementAt(3)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindNonInstantiatedIReadOnlySet() { var dic = new Dictionary @@ -1549,7 +1549,7 @@ public void CanBindNonInstantiatedIReadOnlySet() Assert.Equal("Yo2", options.NonInstantiatedIReadOnlySet.ElementAt(1)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInstantiatedDictionaryOfIReadOnlySetWithSomeExistingValues() { var dic = new Dictionary @@ -1577,7 +1577,7 @@ public void CanBindInstantiatedDictionaryOfIReadOnlySetWithSomeExistingValues() } #endif - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInstantiatedReadOnlyDictionary2() { var dic = new Dictionary @@ -1601,7 +1601,7 @@ public void CanBindInstantiatedReadOnlyDictionary2() } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void BindInstantiatedIReadOnlyDictionary_CreatesCopyOfOriginal() { var dic = new Dictionary @@ -1626,7 +1626,7 @@ public void BindInstantiatedIReadOnlyDictionary_CreatesCopyOfOriginal() Assert.Equal(3, options.Dictionary["item3"]); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void BindNonInstantiatedIReadOnlyDictionary() { var dic = new Dictionary @@ -1647,7 +1647,7 @@ public void BindNonInstantiatedIReadOnlyDictionary() Assert.Equal(2, options.Dictionary["item2"]); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void BindInstantiatedConcreteDictionary_OverwritesOriginal() { var dic = new Dictionary @@ -1671,7 +1671,7 @@ public void BindInstantiatedConcreteDictionary_OverwritesOriginal() Assert.Equal(3, options.Dictionary["item3"]); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInstantiatedReadOnlyDictionary() { var dic = new Dictionary @@ -1694,7 +1694,7 @@ public void CanBindInstantiatedReadOnlyDictionary() Assert.Equal(4, resultingDictionary["item4"]); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindNonInstantiatedReadOnlyDictionary() { var dic = new Dictionary @@ -1714,7 +1714,6 @@ public void CanBindNonInstantiatedReadOnlyDictionary() Assert.Equal(4, options.NonInstantiatedReadOnlyDictionary["item4"]); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] public void CanBindNonInstantiatedDictionaryOfISet() { @@ -1739,7 +1738,7 @@ public void CanBindNonInstantiatedDictionaryOfISet() Assert.Equal("bar-2", options.NonInstantiatedDictionaryWithISet["bar"].ElementAt(1)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInstantiatedDictionaryOfISet() { var dic = new Dictionary @@ -1763,7 +1762,7 @@ public void CanBindInstantiatedDictionaryOfISet() Assert.Equal("bar-2", options.InstantiatedDictionaryWithHashSet["bar"].ElementAt(1)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInstantiatedDictionaryOfISetWithSomeExistingValues() { var dic = new Dictionary @@ -1790,7 +1789,7 @@ public void CanBindInstantiatedDictionaryOfISetWithSomeExistingValues() 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 @@ public void ThrowsForCustomIEnumerableCollection() 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 @@ public void ThrowsForCustomICollection() 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 @@ public void ThrowsForCustomDictionary() 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 @@ public void ThrowsForCustomSet() exception.Message); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInstantiatedISet() { var dic = new Dictionary @@ -1879,7 +1878,7 @@ public void CanBindInstantiatedISet() Assert.Equal("Yo2", options.InstantiatedISet.ElementAt(1)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInstantiatedISetWithSomeValues() { var dic = new Dictionary @@ -1901,7 +1900,7 @@ public void CanBindInstantiatedISetWithSomeValues() Assert.Equal("Yo2", options.InstantiatedISetWithSomeValues.ElementAt(3)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInstantiatedHashSetWithSomeValues() { var dic = new Dictionary @@ -1923,7 +1922,7 @@ public void CanBindInstantiatedHashSetWithSomeValues() Assert.Equal("Yo2", options.InstantiatedHashSetWithSomeValues.ElementAt(3)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindNonInstantiatedHashSet() { var dic = new Dictionary @@ -1943,7 +1942,7 @@ public void CanBindNonInstantiatedHashSet() Assert.Equal("Yo2", options.NonInstantiatedHashSet.ElementAt(1)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInstantiatedSortedSetWithSomeValues() { var dic = new Dictionary @@ -1965,7 +1964,7 @@ public void CanBindInstantiatedSortedSetWithSomeValues() Assert.Equal("Yo2", options.InstantiatedSortedSetWithSomeValues.ElementAt(3)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindNonInstantiatedSortedSetWithSomeValues() { var dic = new Dictionary @@ -1985,7 +1984,7 @@ public void CanBindNonInstantiatedSortedSetWithSomeValues() 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 @@ -2003,7 +2002,7 @@ public void DoesNotBindInstantiatedISetWithUnsupportedKeys() 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 @@ -2021,7 +2020,7 @@ public void DoesNotBindUninstantiatedISetWithUnsupportedKeys() Assert.Null(options.UninstantiatedHashSetWithUnsupportedKey); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInstantiatedIEnumerableWithItems() { var dic = new Dictionary @@ -2041,7 +2040,7 @@ public void CanBindInstantiatedIEnumerableWithItems() Assert.Equal("Yo2", options.InstantiatedIEnumerable.ElementAt(1)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInstantiatedCustomICollectionWithoutAnAddMethodWithItems() { var dic = new Dictionary @@ -2061,7 +2060,7 @@ public void CanBindInstantiatedCustomICollectionWithoutAnAddMethodWithItems() Assert.Equal("Yo2", options.InstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(1)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindNonInstantiatedCustomICollectionWithoutAnAddMethodWithItems() { var dic = new Dictionary @@ -2081,7 +2080,7 @@ public void CanBindNonInstantiatedCustomICollectionWithoutAnAddMethodWithItems() Assert.Equal("Yo2", options.NonInstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(1)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInstantiatedICollectionWithItems() { var dic = new Dictionary @@ -2101,7 +2100,7 @@ public void CanBindInstantiatedICollectionWithItems() Assert.Equal("Yo2", options.InstantiatedICollection.ElementAt(1)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInstantiatedIReadOnlyCollectionWithItems() { var dic = new Dictionary @@ -2121,7 +2120,7 @@ public void CanBindInstantiatedIReadOnlyCollectionWithItems() Assert.Equal("Yo2", options.InstantiatedIReadOnlyCollection.ElementAt(1)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void CanBindInstantiatedIEnumerableWithNullItems() { var dic = new Dictionary @@ -2142,7 +2141,7 @@ public void CanBindInstantiatedIEnumerableWithNullItems() Assert.Equal("Yo2", options.InstantiatedIEnumerable.ElementAt(1)); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void DifferentDictionaryBindingCasesTest() { var dic = new Dictionary() { { "key", "value" } }; @@ -2153,60 +2152,15 @@ public void DifferentDictionaryBindingCasesTest() Assert.Single(config.Get>()); Assert.Single(config.Get>()); Assert.Single(config.Get>()); + // 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>()); - } - - public class OptionsWithDifferentCollectionInterfaces - { - private static IEnumerable s_instantiatedIEnumerable = new List { "value1", "value2" }; - public bool IsSameInstantiatedIEnumerable() => object.ReferenceEquals(s_instantiatedIEnumerable, InstantiatedIEnumerable); - public IEnumerable InstantiatedIEnumerable { get; set; } = s_instantiatedIEnumerable; - - private static IList s_instantiatedIList = new List { "value1", "value2" }; - public bool IsSameInstantiatedIList() => object.ReferenceEquals(s_instantiatedIList, InstantiatedIList); - public IList InstantiatedIList { get; set; } = s_instantiatedIList; - - private static IReadOnlyList s_instantiatedIReadOnlyList = new List { "value1", "value2" }; - public bool IsSameInstantiatedIReadOnlyList() => object.ReferenceEquals(s_instantiatedIReadOnlyList, InstantiatedIReadOnlyList); - public IReadOnlyList InstantiatedIReadOnlyList { get; set; } = s_instantiatedIReadOnlyList; - - private static IDictionary s_instantiatedIDictionary = new Dictionary { ["Key1"] = "value1", ["Key2"] = "value2" }; - public IDictionary InstantiatedIDictionary { get; set; } = s_instantiatedIDictionary; - public bool IsSameInstantiatedIDictionary() => object.ReferenceEquals(s_instantiatedIDictionary, InstantiatedIDictionary); - - private static IReadOnlyDictionary s_instantiatedIReadOnlyDictionary = new Dictionary { ["Key1"] = "value1", ["Key2"] = "value2" }; - public IReadOnlyDictionary InstantiatedIReadOnlyDictionary { get; set; } = s_instantiatedIReadOnlyDictionary; - public bool IsSameInstantiatedIReadOnlyDictionary() => object.ReferenceEquals(s_instantiatedIReadOnlyDictionary, InstantiatedIReadOnlyDictionary); - - private static ISet s_instantiatedISet = new HashSet(StringComparer.OrdinalIgnoreCase) { "a", "A", "b" }; - public ISet InstantiatedISet { get; set; } = s_instantiatedISet; - public bool IsSameInstantiatedISet() => object.ReferenceEquals(s_instantiatedISet, InstantiatedISet); - -#if NETCOREAPP - private static IReadOnlySet s_instantiatedIReadOnlySet = new HashSet(StringComparer.OrdinalIgnoreCase) { "a", "A", "b" }; - public IReadOnlySet InstantiatedIReadOnlySet { get; set; } = s_instantiatedIReadOnlySet; - public bool IsSameInstantiatedIReadOnlySet() => object.ReferenceEquals(s_instantiatedIReadOnlySet, InstantiatedIReadOnlySet); - - public IReadOnlySet UnInstantiatedIReadOnlySet { get; set; } #endif - private static ICollection s_instantiatedICollection = new List { "a", "b", "c" }; - public ICollection InstantiatedICollection { get; set; } = s_instantiatedICollection; - public bool IsSameInstantiatedICollection() => object.ReferenceEquals(s_instantiatedICollection, InstantiatedICollection); - - private static IReadOnlyCollection s_instantiatedIReadOnlyCollection = new List { "a", "b", "c" }; - public IReadOnlyCollection InstantiatedIReadOnlyCollection { get; set; } = s_instantiatedIReadOnlyCollection; - public bool IsSameInstantiatedIReadOnlyCollection() => object.ReferenceEquals(s_instantiatedIReadOnlyCollection, InstantiatedIReadOnlyCollection); - - public IReadOnlyCollection UnInstantiatedIReadOnlyCollection { get; set; } - public ICollection UnInstantiatedICollection { get; set; } - public ISet UnInstantiatedISet { get; set; } - public IReadOnlyDictionary UnInstantiatedIReadOnlyDictionary { get; set; } - public IEnumerable UnInstantiatedIEnumerable { get; set; } - public IList UnInstantiatedIList { get; set; } - public IReadOnlyList UnInstantiatedIReadOnlyList { get; set; } } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void TestOptionsWithDifferentCollectionInterfaces() { var input = new Dictionary @@ -2314,7 +2268,7 @@ public void TestOptionsWithDifferentCollectionInterfaces() Assert.Equal(new string[] { "r", "e" }, options.UnInstantiatedIReadOnlyCollection); } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] + [Fact] public void TestMutatingDictionaryValues() { IConfiguration config = new ConfigurationBuilder() diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.Collections.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.Collections.cs index 9242ae915ab3f7..49d97a79de585b 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.Collections.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.Collections.cs @@ -94,10 +94,6 @@ public class CustomListIndirectlyDerivedFromIEnumerable : IDerivedOne IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } - public class CustomDictionary : Dictionary - { - } - public class NestedOptions { public int Integer { get; set; } @@ -331,5 +327,55 @@ public interface ICustomSet : ISet public interface ICustomDictionary : IDictionary { } + + public class OptionsWithDifferentCollectionInterfaces + { + private static IEnumerable s_instantiatedIEnumerable = new List { "value1", "value2" }; + public bool IsSameInstantiatedIEnumerable() => object.ReferenceEquals(s_instantiatedIEnumerable, InstantiatedIEnumerable); + public IEnumerable InstantiatedIEnumerable { get; set; } = s_instantiatedIEnumerable; + + private static IList s_instantiatedIList = new List { "value1", "value2" }; + public bool IsSameInstantiatedIList() => object.ReferenceEquals(s_instantiatedIList, InstantiatedIList); + public IList InstantiatedIList { get; set; } = s_instantiatedIList; + + private static IReadOnlyList s_instantiatedIReadOnlyList = new List { "value1", "value2" }; + public bool IsSameInstantiatedIReadOnlyList() => object.ReferenceEquals(s_instantiatedIReadOnlyList, InstantiatedIReadOnlyList); + public IReadOnlyList InstantiatedIReadOnlyList { get; set; } = s_instantiatedIReadOnlyList; + + private static IDictionary s_instantiatedIDictionary = new Dictionary { ["Key1"] = "value1", ["Key2"] = "value2" }; + public IDictionary InstantiatedIDictionary { get; set; } = s_instantiatedIDictionary; + public bool IsSameInstantiatedIDictionary() => object.ReferenceEquals(s_instantiatedIDictionary, InstantiatedIDictionary); + + private static IReadOnlyDictionary s_instantiatedIReadOnlyDictionary = new Dictionary { ["Key1"] = "value1", ["Key2"] = "value2" }; + public IReadOnlyDictionary InstantiatedIReadOnlyDictionary { get; set; } = s_instantiatedIReadOnlyDictionary; + public bool IsSameInstantiatedIReadOnlyDictionary() => object.ReferenceEquals(s_instantiatedIReadOnlyDictionary, InstantiatedIReadOnlyDictionary); + + private static ISet s_instantiatedISet = new HashSet(StringComparer.OrdinalIgnoreCase) { "a", "A", "b" }; + public ISet InstantiatedISet { get; set; } = s_instantiatedISet; + public bool IsSameInstantiatedISet() => object.ReferenceEquals(s_instantiatedISet, InstantiatedISet); + +#if NETCOREAPP + private static IReadOnlySet s_instantiatedIReadOnlySet = new HashSet(StringComparer.OrdinalIgnoreCase) { "a", "A", "b" }; + public IReadOnlySet InstantiatedIReadOnlySet { get; set; } = s_instantiatedIReadOnlySet; + public bool IsSameInstantiatedIReadOnlySet() => object.ReferenceEquals(s_instantiatedIReadOnlySet, InstantiatedIReadOnlySet); + + public IReadOnlySet UnInstantiatedIReadOnlySet { get; set; } +#endif + private static ICollection s_instantiatedICollection = new List { "a", "b", "c" }; + public ICollection InstantiatedICollection { get; set; } = s_instantiatedICollection; + public bool IsSameInstantiatedICollection() => object.ReferenceEquals(s_instantiatedICollection, InstantiatedICollection); + + private static IReadOnlyCollection s_instantiatedIReadOnlyCollection = new List { "a", "b", "c" }; + public IReadOnlyCollection InstantiatedIReadOnlyCollection { get; set; } = s_instantiatedIReadOnlyCollection; + public bool IsSameInstantiatedIReadOnlyCollection() => object.ReferenceEquals(s_instantiatedIReadOnlyCollection, InstantiatedIReadOnlyCollection); + + public IReadOnlyCollection UnInstantiatedIReadOnlyCollection { get; set; } + public ICollection UnInstantiatedICollection { get; set; } + public ISet UnInstantiatedISet { get; set; } + public IReadOnlyDictionary UnInstantiatedIReadOnlyDictionary { get; set; } + public IEnumerable UnInstantiatedIEnumerable { get; set; } + public IList UnInstantiatedIList { get; set; } + public IReadOnlyList UnInstantiatedIReadOnlyList { get; set; } + } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs index c025e194c38cde..121d9a908606eb 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs @@ -941,8 +941,8 @@ public void ExceptionWhenTryingToBindClassWherePropertiesDoMatchConstructorParam 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 { @@ -961,8 +961,8 @@ public void ExceptionWhenTryingToBindClassWherePropertiesDoMatchConstructorParam 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 { @@ -982,8 +982,8 @@ public void ExceptionWhenTryingToBindClassWherePropertiesDoMatchConstructorParam 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 { @@ -1520,7 +1520,7 @@ public void CanBindNullableNestedStructProperties() 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 @@ public void PrivatePropertiesFromBaseClass_Get() #endif } - [ConditionalFact(typeof(TestHelpers), nameof(TestHelpers.NotSourceGenMode))] // Need collection support. + [Fact] public void EnsureCallingThePropertySetter() { var json = @"{ @@ -1616,7 +1616,13 @@ public void EnsureCallingThePropertySetter() 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 index 00000000000000..cba992ebfc1bb6 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/TestCollectionsGen.generated.txt @@ -0,0 +1,240 @@ +// +#nullable enable + +internal static class GeneratedConfigurationBinder +{ + public static T? Get(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? 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 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 obj, BinderOptions? binderOptions) + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + if (obj is not ICollection 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 obj, BinderOptions? binderOptions) + { + if (obj is null) + { + throw new ArgumentNullException(nameof(obj)); + } + + if (obj is not IDictionary 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? temp = null; + foreach (IConfigurationSection section in configuration.GetChildren()) + { + switch (section.Key) + { + case "CustomDictionary": + { + if (HasChildren(section)) + { + Program.CustomDictionary temp5 = obj.CustomDictionary; + temp5 ??= new Program.CustomDictionary(); + 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 temp7 = obj.IReadOnlyList; + temp7 = temp7 is null ? new List() : new List(temp7); + BindCore(section, ref temp7, binderOptions); + obj.IReadOnlyList = temp7; + } + } + break; + case "UnsupportedIReadOnlyDictionaryUnsupported": + { + } + break; + case "IReadOnlyDictionary": + { + if (HasChildren(section)) + { + IReadOnlyDictionary temp8 = obj.IReadOnlyDictionary; + temp8 = temp8 is null ? new Dictionary() : 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()).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 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); + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs index 6359acb9bc44ad..cbc6b50ee31873 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/ConfingurationBindingSourceGeneratorTests.cs @@ -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 static void Main() config.Bind(configObj, options => { }) config.Bind(""key"", configObj); } - + public class MyClass { public string MyString { get; set; } @@ -75,7 +77,7 @@ public static void Main() configObj = config.Get(binderOptions => { }); configObj = config.Get(typeof(MyClass2), binderOptions => { }); } - + public class MyClass { public string MyString { get; set; } @@ -159,7 +161,7 @@ public static void Main() ServiceCollection services = new(); services.Configure(section); } - + public class MyClass { public string MyString { get; set; } @@ -249,19 +251,84 @@ public async Task LangVersionMustBeCharp11OrHigher() 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(); + } + + public class MyClassWithCustomCollections + { + public CustomDictionary CustomDictionary { get; set; } + public CustomList CustomList { get; set; } + public ICustomDictionary ICustomDictionary { get; set; } + public ICustomSet ICustomCollection { get; set; } + public IReadOnlyList IReadOnlyList { get; set; } + public IReadOnlyDictionary UnsupportedIReadOnlyDictionaryUnsupported { get; set; } + public IReadOnlyDictionary IReadOnlyDictionary { get; set; } + } + + public class CustomDictionary : Dictionary + { + } + + public class CustomList : List + { + } + + public interface ICustomDictionary : IDictionary + { + } + + public interface ICustomSet : ISet + { + } + } + """; + + 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 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>? 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);