diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs index 81fb4985537210..0982628197ea65 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs @@ -197,7 +197,7 @@ private TypeSpec CreateTypeSpec(TypeParseInfo typeParseInfo) } else if (IsCollection(type)) { - spec = CreateCollectionSpec(typeParseInfo); + spec = CreateCollectionSpec(typeParseInfo) ?? CreateObjectSpec(typeParseInfo); } else if (SymbolEqualityComparer.Default.Equals(type, _typeSymbols.IConfigurationSection)) { @@ -360,34 +360,37 @@ private TypeSpec CreateArraySpec(TypeParseInfo typeParseInfo) }; } - private TypeSpec CreateCollectionSpec(TypeParseInfo typeParseInfo) + private TypeSpec? CreateCollectionSpec(TypeParseInfo typeParseInfo) { INamedTypeSymbol type = (INamedTypeSymbol)typeParseInfo.TypeSymbol; - TypeSpec spec; + TypeSpec? spec; if (IsCandidateDictionary(type, out ITypeSymbol? keyType, out ITypeSymbol? elementType)) { spec = CreateDictionarySpec(typeParseInfo, keyType, elementType); - Debug.Assert(spec is DictionarySpec or UnsupportedTypeSpec); + Debug.Assert(spec is DictionarySpec or UnsupportedTypeSpec or null); } else { spec = CreateEnumerableSpec(typeParseInfo); - Debug.Assert(spec is EnumerableSpec or UnsupportedTypeSpec); + Debug.Assert(spec is EnumerableSpec or UnsupportedTypeSpec or null); } return spec; } - private TypeSpec CreateDictionarySpec(TypeParseInfo typeParseInfo, ITypeSymbol keyTypeSymbol, ITypeSymbol elementTypeSymbol) + private TypeSpec? CreateDictionarySpec(TypeParseInfo typeParseInfo, ITypeSymbol keyTypeSymbol, ITypeSymbol elementTypeSymbol) { + INamedTypeSymbol type = (INamedTypeSymbol)typeParseInfo.TypeSymbol; + + // treat as unsupported if it implements IDictionary<,>, otherwise we'll try to fallback and treat as an object + bool isDictionary = _typeSymbols.GenericICollection is not null && GetInterface(type, _typeSymbols.GenericIDictionary_Unbound) is not null; + if (IsUnsupportedType(keyTypeSymbol) || IsUnsupportedType(elementTypeSymbol)) { - return CreateUnsupportedCollectionSpec(typeParseInfo); + return isDictionary ? CreateUnsupportedCollectionSpec(typeParseInfo) : null; } - INamedTypeSymbol type = (INamedTypeSymbol)typeParseInfo.TypeSymbol; - CollectionInstantiationStrategy instantiationStrategy; CollectionInstantiationConcreteType instantiationConcreteType; CollectionPopulationCastType populationCastType; @@ -402,14 +405,15 @@ private TypeSpec CreateDictionarySpec(TypeParseInfo typeParseInfo, ITypeSymbol k { populationCastType = CollectionPopulationCastType.NotApplicable; } - else if (_typeSymbols.GenericIDictionary is not null && GetInterface(type, _typeSymbols.GenericIDictionary_Unbound) is not null) + else if (isDictionary) { // implements IDictionary<,> -- cast to it. populationCastType = CollectionPopulationCastType.IDictionary; } else { - return CreateUnsupportedCollectionSpec(typeParseInfo); + // not a dictionary + return null; } } else if (_typeSymbols.Dictionary is not null && @@ -429,7 +433,7 @@ private TypeSpec CreateDictionarySpec(TypeParseInfo typeParseInfo, ITypeSymbol k } else { - return CreateUnsupportedCollectionSpec(typeParseInfo); + return isDictionary ? CreateUnsupportedCollectionSpec(typeParseInfo) : null; } TypeRef keyTypeRef = EnqueueTransitiveType(typeParseInfo, keyTypeSymbol, DiagnosticDescriptors.DictionaryKeyNotSupported); @@ -447,18 +451,20 @@ private TypeSpec CreateDictionarySpec(TypeParseInfo typeParseInfo, ITypeSymbol k }; } - private TypeSpec CreateEnumerableSpec(TypeParseInfo typeParseInfo) + private TypeSpec? CreateEnumerableSpec(TypeParseInfo typeParseInfo) { INamedTypeSymbol type = (INamedTypeSymbol)typeParseInfo.TypeSymbol; + bool isCollection = _typeSymbols.GenericICollection is not null && GetInterface(type, _typeSymbols.GenericICollection_Unbound) is not null; + if (!TryGetElementType(type, out ITypeSymbol? elementType)) { - return CreateUnsupportedCollectionSpec(typeParseInfo); + return isCollection ? CreateUnsupportedCollectionSpec(typeParseInfo) : null; } if (IsUnsupportedType(elementType)) { - return CreateUnsupportedCollectionSpec(typeParseInfo); + return isCollection ? CreateUnsupportedCollectionSpec(typeParseInfo) : null; } CollectionInstantiationStrategy instantiationStrategy; @@ -475,14 +481,15 @@ private TypeSpec CreateEnumerableSpec(TypeParseInfo typeParseInfo) { populationCastType = CollectionPopulationCastType.NotApplicable; } - else if (_typeSymbols.GenericICollection is not null && GetInterface(type, _typeSymbols.GenericICollection_Unbound) is not null) + else if (isCollection) { // implements ICollection<> -- cast to it populationCastType = CollectionPopulationCastType.ICollection; } else { - return CreateUnsupportedCollectionSpec(typeParseInfo); + // not a collection + return null; } } else if ((IsInterfaceMatch(type, _typeSymbols.GenericICollection_Unbound) || IsInterfaceMatch(type, _typeSymbols.GenericIList_Unbound))) @@ -523,7 +530,7 @@ private TypeSpec CreateEnumerableSpec(TypeParseInfo typeParseInfo) } else { - return CreateUnsupportedCollectionSpec(typeParseInfo); + return isCollection ? CreateUnsupportedCollectionSpec(typeParseInfo) : null; } TypeRef elementTypeRef = EnqueueTransitiveType(typeParseInfo, elementType, DiagnosticDescriptors.ElementTypeNotSupported); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs index 0e6e09e637c0e0..28209c1c5deb7b 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; @@ -1067,5 +1068,25 @@ public class DerivedClassWithHiddenMembers : IntermediateDerivedClass public override int X { set => base.X = value + 1; } } + public class EnumerableNotCollection : IEnumerable> + { + public string Names { get; set; } + + public string[] Keywords { get; set; } + + public bool Enabled { get; set; } + + private IEnumerable> enumerate() + { + yield return new KeyValuePair(nameof(Names), Names); + yield return new KeyValuePair(nameof(Keywords), string.Join(",", Keywords)); + yield return new KeyValuePair(nameof(Enabled), Enabled.ToString()); + } + + public IEnumerator> GetEnumerator() => enumerate().GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => enumerate().GetEnumerator(); + } + } } 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 406738478d1c55..6ce085d5ec9cde 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs @@ -2602,5 +2602,27 @@ public void CanBindToClassWithNewProperties() Assert.Equal(53, obj.X); Assert.Equal(53, obj.XBase); } + + [Fact] + public void CanGetEnumerableNotCollection() + { + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(new KeyValuePair[] + { + new("Names", "John,Jane,Stephen"), + new("Enabled", "true"), + new("Keywords:1", "new"), + new("Keywords:2", "class"), + new("Keywords:3", "rosebud") + }); + + var config = builder.Build(); + + var result = config.Get(); + + Assert.Equal("John,Jane,Stephen", result.Names); + Assert.True(result.Enabled); + Assert.Equal(new [] { "new", "class", "rosebud"}, result.Keywords); + } } }