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 3d6e2b6503101a..81fb4985537210 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/ConfigurationBindingGenerator.Parser.cs @@ -391,6 +391,7 @@ private TypeSpec CreateDictionarySpec(TypeParseInfo typeParseInfo, ITypeSymbol k CollectionInstantiationStrategy instantiationStrategy; CollectionInstantiationConcreteType instantiationConcreteType; CollectionPopulationCastType populationCastType; + bool shouldTryCast = false; if (HasPublicParameterLessCtor(type)) { @@ -403,6 +404,7 @@ private TypeSpec CreateDictionarySpec(TypeParseInfo typeParseInfo, ITypeSymbol k } else if (_typeSymbols.GenericIDictionary is not null && GetInterface(type, _typeSymbols.GenericIDictionary_Unbound) is not null) { + // implements IDictionary<,> -- cast to it. populationCastType = CollectionPopulationCastType.IDictionary; } else @@ -421,7 +423,9 @@ private TypeSpec CreateDictionarySpec(TypeParseInfo typeParseInfo, ITypeSymbol k { instantiationStrategy = CollectionInstantiationStrategy.LinqToDictionary; instantiationConcreteType = CollectionInstantiationConcreteType.Dictionary; + // is IReadonlyDictionary<,> -- test cast to IDictionary<,> populationCastType = CollectionPopulationCastType.IDictionary; + shouldTryCast = true; } else { @@ -431,6 +435,7 @@ private TypeSpec CreateDictionarySpec(TypeParseInfo typeParseInfo, ITypeSymbol k TypeRef keyTypeRef = EnqueueTransitiveType(typeParseInfo, keyTypeSymbol, DiagnosticDescriptors.DictionaryKeyNotSupported); TypeRef elementTypeRef = EnqueueTransitiveType(typeParseInfo, elementTypeSymbol, DiagnosticDescriptors.ElementTypeNotSupported); + Debug.Assert(!shouldTryCast || !type.IsValueType, "Should not test cast for value types."); return new DictionarySpec(type) { KeyTypeRef = keyTypeRef, @@ -438,6 +443,7 @@ private TypeSpec CreateDictionarySpec(TypeParseInfo typeParseInfo, ITypeSymbol k InstantiationStrategy = instantiationStrategy, InstantiationConcreteType = instantiationConcreteType, PopulationCastType = populationCastType, + ShouldTryCast = shouldTryCast }; } @@ -458,6 +464,7 @@ private TypeSpec CreateEnumerableSpec(TypeParseInfo typeParseInfo) CollectionInstantiationStrategy instantiationStrategy; CollectionInstantiationConcreteType instantiationConcreteType; CollectionPopulationCastType populationCastType; + bool shouldTryCast = false; if (HasPublicParameterLessCtor(type)) { @@ -470,6 +477,7 @@ private TypeSpec CreateEnumerableSpec(TypeParseInfo typeParseInfo) } else if (_typeSymbols.GenericICollection is not null && GetInterface(type, _typeSymbols.GenericICollection_Unbound) is not null) { + // implements ICollection<> -- cast to it populationCastType = CollectionPopulationCastType.ICollection; } else @@ -487,7 +495,9 @@ private TypeSpec CreateEnumerableSpec(TypeParseInfo typeParseInfo) { instantiationStrategy = CollectionInstantiationStrategy.CopyConstructor; instantiationConcreteType = CollectionInstantiationConcreteType.List; + // is IEnumerable<> -- test cast to ICollection<> populationCastType = CollectionPopulationCastType.ICollection; + shouldTryCast = true; } else if (IsInterfaceMatch(type, _typeSymbols.ISet_Unbound)) { @@ -499,13 +509,17 @@ private TypeSpec CreateEnumerableSpec(TypeParseInfo typeParseInfo) { instantiationStrategy = CollectionInstantiationStrategy.CopyConstructor; instantiationConcreteType = CollectionInstantiationConcreteType.HashSet; + // is IReadOnlySet<> -- test cast to ISet<> populationCastType = CollectionPopulationCastType.ISet; + shouldTryCast = true; } else if (IsInterfaceMatch(type, _typeSymbols.IReadOnlyList_Unbound) || IsInterfaceMatch(type, _typeSymbols.IReadOnlyCollection_Unbound)) { instantiationStrategy = CollectionInstantiationStrategy.CopyConstructor; instantiationConcreteType = CollectionInstantiationConcreteType.List; + // is IReadOnlyList<> or IReadOnlyCollection<> -- test cast to ICollection<> populationCastType = CollectionPopulationCastType.ICollection; + shouldTryCast = true; } else { @@ -514,12 +528,14 @@ private TypeSpec CreateEnumerableSpec(TypeParseInfo typeParseInfo) TypeRef elementTypeRef = EnqueueTransitiveType(typeParseInfo, elementType, DiagnosticDescriptors.ElementTypeNotSupported); + Debug.Assert(!shouldTryCast || !type.IsValueType, "Should not test cast for value types."); return new EnumerableSpec(type) { ElementTypeRef = elementTypeRef, InstantiationStrategy = instantiationStrategy, InstantiationConcreteType = instantiationConcreteType, PopulationCastType = populationCastType, + ShouldTryCast = shouldTryCast }; } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs index 84446ae68a5acb..3433544bf9ee74 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs @@ -1230,15 +1230,25 @@ private void EmitCollectionCastIfRequired(CollectionWithCtorInitSpec type, out s return; } - string castTypeDisplayString = TypeIndex.GetPopulationCastTypeDisplayString(type); instanceIdentifier = Identifier.temp; + string castExpression = $"{TypeIndex.GetPopulationCastTypeDisplayString(type)} {instanceIdentifier}"; + + if (type.ShouldTryCast) + { + _writer.WriteLine($$""" + if ({{Identifier.instance}} is not {{castExpression}}) + { + return; + } + """); + } + else + { + _writer.WriteLine($$""" + {{castExpression}} = {{Identifier.instance}}; + """); + } - _writer.WriteLine($$""" - if ({{Identifier.instance}} is not {{castTypeDisplayString}} {{instanceIdentifier}}) - { - return; - } - """); _writer.WriteLine(); } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/CollectionSpec.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/CollectionSpec.cs index f891328f77af7c..f34716d37da74f 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/CollectionSpec.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/Types/CollectionSpec.cs @@ -23,6 +23,8 @@ protected CollectionWithCtorInitSpec(ITypeSymbol type) : base(type) { } public required CollectionInstantiationConcreteType InstantiationConcreteType { get; init; } public required CollectionPopulationCastType PopulationCastType { get; init; } + + public required bool ShouldTryCast { get; init; } } internal sealed record ArraySpec : CollectionSpec 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 06b44553ab9fd5..14b164b3f0acda 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 @@ -2368,6 +2368,83 @@ public void TestCollectionWithNullOrEmptyItems() Assert.Equal("System.Boolean", result[0].Elements[1].Type); } + [Fact] + public void TestStringValues() + { + // StringValues is a struct that implements IList -- though it doesn't actually support Add + + var dic = new Dictionary + { + {"StringValues:0", "Yo1"}, + {"StringValues:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = new OptionsWithStructs(); + +#if BUILDING_SOURCE_GENERATOR_TESTS + Assert.Throws(() => config.Bind(options)); +#else + Assert.Throws(() => config.Bind(options, (bo) => bo.ErrorOnUnknownConfiguration = true)); +#endif + } + + [Fact] + public void TestOptionsWithStructs() + { + var dic = new Dictionary + { + {"CollectionStructExplicit:0", "cs1"}, + {"CollectionStructExplicit:1", "cs2"}, + {"DictionaryStructExplicit:k0", "ds1"}, + {"DictionaryStructExplicit:k1", "ds2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = new OptionsWithStructs(); + config.Bind(options); + + ICollection collection = options.CollectionStructExplicit; + Assert.Equal(2, collection.Count); + Assert.Equal(collection, ["cs1", "cs2"]); + + IDictionary dictionary = options.DictionaryStructExplicit; + Assert.Equal(2, dictionary.Count); + Assert.Equal("ds1", dictionary["k0"]); + Assert.Equal("ds2", dictionary["k1"]); + } + + [Fact] + public void TestOptionsWithUnsupportedStructs() + { + var dic = new Dictionary + { + {"ReadOnlyCollectionStructExplicit:0", "cs1"}, + {"ReadOnlyCollectionStructExplicit:1", "cs2"}, + {"ReadOnlyDictionaryStructExplicit:k0", "ds1"}, + {"ReadOnlyDictionaryStructExplicit:k1", "ds2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = new OptionsWithUnsupportedStructs(); + config.Bind(options); + + IReadOnlyCollection collection = options.ReadOnlyCollectionStructExplicit; + Assert.Equal(0, collection.Count); + + IReadOnlyDictionary dictionary = options.ReadOnlyDictionaryStructExplicit; + Assert.Equal(0, dictionary.Count); + } + // Test behavior for root level arrays. // Tests for TypeConverter usage. 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 30d1fc8dc5b8c0..5b6bdc44e0db03 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 @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using Microsoft.Extensions.Primitives; namespace Microsoft.Extensions #if BUILDING_SOURCE_GENERATOR_TESTS @@ -388,5 +389,123 @@ public class Element { public string Type { get; set; } } + + public class OptionsWithStructs + { + public StringValues StringValues { get; set; } + + public CollectionStructExplicit CollectionStructExplicit { get; set; } = new(); + + public DictionaryStructExplicit DictionaryStructExplicit { get; set; } = new(); + } + + public struct CollectionStructExplicit : ICollection + { + public CollectionStructExplicit() {} + + ICollection _collection = new List(); + + int ICollection.Count => _collection.Count; + + bool ICollection.IsReadOnly => _collection.IsReadOnly; + + void ICollection.Add(string item) => _collection.Add(item); + + void ICollection.Clear() => _collection.Clear(); + + bool ICollection.Contains(string item) => _collection.Contains(item); + + void ICollection.CopyTo(string[] array, int arrayIndex) => _collection.CopyTo(array, arrayIndex); + + IEnumerator IEnumerable.GetEnumerator() => _collection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_collection).GetEnumerator(); + + bool ICollection.Remove(string item) => _collection.Remove(item); + } + + public struct DictionaryStructExplicit : IDictionary + { + public DictionaryStructExplicit() {} + + IDictionary _dictionary = new Dictionary(); + + string IDictionary.this[string key] { get => _dictionary[key]; set => _dictionary[key] = value; } + + ICollection IDictionary.Keys => _dictionary.Keys; + + ICollection IDictionary.Values => _dictionary.Values; + + int ICollection>.Count => _dictionary.Count; + + bool ICollection>.IsReadOnly => _dictionary.IsReadOnly; + + void IDictionary.Add(string key, string value) => _dictionary.Add(key, value); + + void ICollection>.Add(KeyValuePair item) => _dictionary.Add(item); + + void ICollection>.Clear() => _dictionary.Clear(); + + bool ICollection>.Contains(KeyValuePair item) => _dictionary.Contains(item); + + bool IDictionary.ContainsKey(string key) => _dictionary.ContainsKey(key); + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => _dictionary.CopyTo(array, arrayIndex); + + IEnumerator> IEnumerable>.GetEnumerator() => _dictionary.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_dictionary).GetEnumerator(); + + bool IDictionary.Remove(string key) => _dictionary.Remove(key); + + bool ICollection>.Remove(KeyValuePair item) => _dictionary.Remove(item); + + bool IDictionary.TryGetValue(string key, out string value) => _dictionary.TryGetValue(key, out value); + } + + public class OptionsWithUnsupportedStructs + { + public ReadOnlyCollectionStructExplicit ReadOnlyCollectionStructExplicit { get; set; } = new(); + + public ReadOnlyDictionaryStructExplicit ReadOnlyDictionaryStructExplicit { get; set; } = new(); + } + + public struct ReadOnlyCollectionStructExplicit : IReadOnlyCollection + { + public ReadOnlyCollectionStructExplicit() + { + _collection = new List(); + } + + private readonly IReadOnlyCollection _collection; + int IReadOnlyCollection.Count => _collection.Count; + IEnumerator IEnumerable.GetEnumerator() => _collection.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_collection).GetEnumerator(); + } + + public struct ReadOnlyDictionaryStructExplicit : IReadOnlyDictionary + { + public ReadOnlyDictionaryStructExplicit() + { + _dictionary = new Dictionary(); + } + + private readonly IReadOnlyDictionary _dictionary; + string IReadOnlyDictionary.this[string key] => _dictionary[key]; + + IEnumerable IReadOnlyDictionary.Keys => _dictionary.Keys; + + IEnumerable IReadOnlyDictionary.Values => _dictionary.Values; + + int IReadOnlyCollection>.Count => _dictionary.Count; + + bool IReadOnlyDictionary.ContainsKey(string key) => _dictionary.ContainsKey(key); + + IEnumerator> IEnumerable>.GetEnumerator() => _dictionary.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_dictionary).GetEnumerator(); + + bool IReadOnlyDictionary.TryGetValue(string key, out string value) => _dictionary.TryGetValue(key, out value); + } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/net462/Version0/Collections.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/net462/Version0/Collections.generated.txt index 54c0b42cc3f050..b241f66b2fc8dc 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/net462/Version0/Collections.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/net462/Version0/Collections.generated.txt @@ -36,12 +36,12 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { #region IConfiguration extensions. /// Attempts to bind the configuration instance to a new instance of type T. - [InterceptsLocation(@"src-0.cs", 12, 17)] + [InterceptsLocation(@"src-0.cs", 13, 17)] public static T? Get(this IConfiguration configuration) => (T?)(GetCore(configuration, typeof(T), configureOptions: null) ?? default(T)); #endregion IConfiguration extensions. #region Core binding extensions. - private readonly static Lazy> s_configKeys_ProgramMyClassWithCustomCollections = new(() => new HashSet(StringComparer.OrdinalIgnoreCase) { "CustomDictionary", "CustomList", "ICustomDictionary", "ICustomCollection", "IReadOnlyList", "UnsupportedIReadOnlyDictionaryUnsupported", "IReadOnlyDictionary" }); + private readonly static Lazy> s_configKeys_ProgramMyClassWithCustomCollections = new(() => new HashSet(StringComparer.OrdinalIgnoreCase) { "CustomDictionary", "CustomList", "ICustomDictionary", "ICustomCollection", "IReadOnlyList", "UnsupportedIReadOnlyDictionaryUnsupported", "IReadOnlyDictionary", "CollectionStructExplicit" }); public static object? GetCore(this IConfiguration configuration, Type type, Action? configureOptions) { @@ -121,6 +121,19 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration } } + public static void BindCore(IConfiguration configuration, ref global::Program.CollectionStructExplicit instance, bool defaultValueIfNotFound, BinderOptions? binderOptions) + { + ICollection temp = instance; + + foreach (IConfigurationSection section in configuration.GetChildren()) + { + if (section.Value is string value) + { + temp.Add(value); + } + } + } + public static void BindCore(IConfiguration configuration, ref global::Program.MyClassWithCustomCollections instance, bool defaultValueIfNotFound, BinderOptions? binderOptions) { ValidateConfigurationKeys(typeof(global::Program.MyClassWithCustomCollections), s_configKeys_ProgramMyClassWithCustomCollections, configuration, binderOptions); @@ -156,6 +169,15 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration BindCore(section10, ref temp12, defaultValueIfNotFound: false, binderOptions); instance.IReadOnlyDictionary = temp12; } + + if (AsConfigWithChildren(configuration.GetSection("CollectionStructExplicit")) is IConfigurationSection section13) + { + global::Program.CollectionStructExplicit temp14 = instance.CollectionStructExplicit; + var temp15 = new global::Program.CollectionStructExplicit(); + BindCore(section13, ref temp15, defaultValueIfNotFound: false, binderOptions); + instance.CollectionStructExplicit = temp15; + temp14 = temp15; + } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/net462/Version1/Collections.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/net462/Version1/Collections.generated.txt index 14f60bde81175d..c261c340aaf256 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/net462/Version1/Collections.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/net462/Version1/Collections.generated.txt @@ -36,12 +36,12 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { #region IConfiguration extensions. /// Attempts to bind the configuration instance to a new instance of type T. - [InterceptsLocation(1, "x0/k4h2yDpHi1YoDOFMOUFoBAABzcmMtMC5jcw==")] // src-0.cs(12,17) + [InterceptsLocation(1, "2Ny1FTOTCAvRq9jRD2PynXQBAABzcmMtMC5jcw==")] // src-0.cs(13,17) public static T? Get(this IConfiguration configuration) => (T?)(GetCore(configuration, typeof(T), configureOptions: null) ?? default(T)); #endregion IConfiguration extensions. #region Core binding extensions. - private readonly static Lazy> s_configKeys_ProgramMyClassWithCustomCollections = new(() => new HashSet(StringComparer.OrdinalIgnoreCase) { "CustomDictionary", "CustomList", "ICustomDictionary", "ICustomCollection", "IReadOnlyList", "UnsupportedIReadOnlyDictionaryUnsupported", "IReadOnlyDictionary" }); + private readonly static Lazy> s_configKeys_ProgramMyClassWithCustomCollections = new(() => new HashSet(StringComparer.OrdinalIgnoreCase) { "CustomDictionary", "CustomList", "ICustomDictionary", "ICustomCollection", "IReadOnlyList", "UnsupportedIReadOnlyDictionaryUnsupported", "IReadOnlyDictionary", "CollectionStructExplicit" }); public static object? GetCore(this IConfiguration configuration, Type type, Action? configureOptions) { @@ -121,6 +121,19 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration } } + public static void BindCore(IConfiguration configuration, ref global::Program.CollectionStructExplicit instance, bool defaultValueIfNotFound, BinderOptions? binderOptions) + { + ICollection temp = instance; + + foreach (IConfigurationSection section in configuration.GetChildren()) + { + if (section.Value is string value) + { + temp.Add(value); + } + } + } + public static void BindCore(IConfiguration configuration, ref global::Program.MyClassWithCustomCollections instance, bool defaultValueIfNotFound, BinderOptions? binderOptions) { ValidateConfigurationKeys(typeof(global::Program.MyClassWithCustomCollections), s_configKeys_ProgramMyClassWithCustomCollections, configuration, binderOptions); @@ -156,6 +169,15 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration BindCore(section10, ref temp12, defaultValueIfNotFound: false, binderOptions); instance.IReadOnlyDictionary = temp12; } + + if (AsConfigWithChildren(configuration.GetSection("CollectionStructExplicit")) is IConfigurationSection section13) + { + global::Program.CollectionStructExplicit temp14 = instance.CollectionStructExplicit; + var temp15 = new global::Program.CollectionStructExplicit(); + BindCore(section13, ref temp15, defaultValueIfNotFound: false, binderOptions); + instance.CollectionStructExplicit = temp15; + temp14 = temp15; + } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/netcoreapp/Version0/Collections.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/netcoreapp/Version0/Collections.generated.txt index b8d24de4a64e74..96bf9cba271a87 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/netcoreapp/Version0/Collections.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/netcoreapp/Version0/Collections.generated.txt @@ -36,12 +36,12 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { #region IConfiguration extensions. /// Attempts to bind the configuration instance to a new instance of type T. - [InterceptsLocation(@"src-0.cs", 12, 17)] + [InterceptsLocation(@"src-0.cs", 13, 17)] public static T? Get(this IConfiguration configuration) => (T?)(GetCore(configuration, typeof(T), configureOptions: null) ?? default(T)); #endregion IConfiguration extensions. #region Core binding extensions. - private readonly static Lazy> s_configKeys_ProgramMyClassWithCustomCollections = new(() => new HashSet(StringComparer.OrdinalIgnoreCase) { "CustomDictionary", "CustomList", "ICustomDictionary", "ICustomCollection", "IReadOnlyList", "UnsupportedIReadOnlyDictionaryUnsupported", "IReadOnlyDictionary" }); + private readonly static Lazy> s_configKeys_ProgramMyClassWithCustomCollections = new(() => new HashSet(StringComparer.OrdinalIgnoreCase) { "CustomDictionary", "CustomList", "ICustomDictionary", "ICustomCollection", "IReadOnlyList", "UnsupportedIReadOnlyDictionaryUnsupported", "IReadOnlyDictionary", "CollectionStructExplicit" }); public static object? GetCore(this IConfiguration configuration, Type type, Action? configureOptions) { @@ -118,6 +118,19 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration } } + public static void BindCore(IConfiguration configuration, ref global::Program.CollectionStructExplicit instance, bool defaultValueIfNotFound, BinderOptions? binderOptions) + { + ICollection temp = instance; + + foreach (IConfigurationSection section in configuration.GetChildren()) + { + if (section.Value is string value) + { + temp.Add(value); + } + } + } + public static void BindCore(IConfiguration configuration, ref global::Program.MyClassWithCustomCollections instance, bool defaultValueIfNotFound, BinderOptions? binderOptions) { ValidateConfigurationKeys(typeof(global::Program.MyClassWithCustomCollections), s_configKeys_ProgramMyClassWithCustomCollections, configuration, binderOptions); @@ -153,6 +166,15 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration BindCore(section10, ref temp12, defaultValueIfNotFound: false, binderOptions); instance.IReadOnlyDictionary = temp12; } + + if (AsConfigWithChildren(configuration.GetSection("CollectionStructExplicit")) is IConfigurationSection section13) + { + global::Program.CollectionStructExplicit temp14 = instance.CollectionStructExplicit; + var temp15 = new global::Program.CollectionStructExplicit(); + BindCore(section13, ref temp15, defaultValueIfNotFound: false, binderOptions); + instance.CollectionStructExplicit = temp15; + temp14 = temp15; + } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/netcoreapp/Version1/Collections.generated.txt b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/netcoreapp/Version1/Collections.generated.txt index 9b690cd988893b..8c25f92341eba5 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/netcoreapp/Version1/Collections.generated.txt +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/Baselines/netcoreapp/Version1/Collections.generated.txt @@ -36,12 +36,12 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration { #region IConfiguration extensions. /// Attempts to bind the configuration instance to a new instance of type T. - [InterceptsLocation(1, "x0/k4h2yDpHi1YoDOFMOUFoBAABzcmMtMC5jcw==")] // src-0.cs(12,17) + [InterceptsLocation(1, "2Ny1FTOTCAvRq9jRD2PynXQBAABzcmMtMC5jcw==")] // src-0.cs(13,17) public static T? Get(this IConfiguration configuration) => (T?)(GetCore(configuration, typeof(T), configureOptions: null) ?? default(T)); #endregion IConfiguration extensions. #region Core binding extensions. - private readonly static Lazy> s_configKeys_ProgramMyClassWithCustomCollections = new(() => new HashSet(StringComparer.OrdinalIgnoreCase) { "CustomDictionary", "CustomList", "ICustomDictionary", "ICustomCollection", "IReadOnlyList", "UnsupportedIReadOnlyDictionaryUnsupported", "IReadOnlyDictionary" }); + private readonly static Lazy> s_configKeys_ProgramMyClassWithCustomCollections = new(() => new HashSet(StringComparer.OrdinalIgnoreCase) { "CustomDictionary", "CustomList", "ICustomDictionary", "ICustomCollection", "IReadOnlyList", "UnsupportedIReadOnlyDictionaryUnsupported", "IReadOnlyDictionary", "CollectionStructExplicit" }); public static object? GetCore(this IConfiguration configuration, Type type, Action? configureOptions) { @@ -118,6 +118,19 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration } } + public static void BindCore(IConfiguration configuration, ref global::Program.CollectionStructExplicit instance, bool defaultValueIfNotFound, BinderOptions? binderOptions) + { + ICollection temp = instance; + + foreach (IConfigurationSection section in configuration.GetChildren()) + { + if (section.Value is string value) + { + temp.Add(value); + } + } + } + public static void BindCore(IConfiguration configuration, ref global::Program.MyClassWithCustomCollections instance, bool defaultValueIfNotFound, BinderOptions? binderOptions) { ValidateConfigurationKeys(typeof(global::Program.MyClassWithCustomCollections), s_configKeys_ProgramMyClassWithCustomCollections, configuration, binderOptions); @@ -153,6 +166,15 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration BindCore(section10, ref temp12, defaultValueIfNotFound: false, binderOptions); instance.IReadOnlyDictionary = temp12; } + + if (AsConfigWithChildren(configuration.GetSection("CollectionStructExplicit")) is IConfigurationSection section13) + { + global::Program.CollectionStructExplicit temp14 = instance.CollectionStructExplicit; + var temp15 = new global::Program.CollectionStructExplicit(); + BindCore(section13, ref temp15, defaultValueIfNotFound: false, binderOptions); + instance.CollectionStructExplicit = temp15; + temp14 = temp15; + } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs index 602f1e66be7a63..0865696c2edfab 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Baselines.cs @@ -863,6 +863,7 @@ public ClassWhereParametersHaveDefaultValue(string? name = "John Doe", string ad public async Task Collections() { string source = """ + using System.Collections; using System.Collections.Generic; using Microsoft.Extensions.Configuration; @@ -888,6 +889,7 @@ public class MyClassWithCustomCollections // Diagnostic warning because we don't know how to instantiate the property type. public IReadOnlyDictionary UnsupportedIReadOnlyDictionaryUnsupported { get; set; } public IReadOnlyDictionary IReadOnlyDictionary { get; set; } + public CollectionStructExplicit CollectionStructExplicit { get; set; } } public class CustomDictionary : Dictionary @@ -907,6 +909,32 @@ public interface ICustomDictionary : IDictionary public interface ICustomSet : ISet { } + + // struct that explicitly implements ICollection + public struct CollectionStructExplicit : ICollection + { + public CollectionStructExplicit() {} + + ICollection _collection = new List(); + + int ICollection.Count => _collection.Count; + + bool ICollection.IsReadOnly => _collection.IsReadOnly; + + void ICollection.Add(string item) => _collection.Add(item); + + void ICollection.Clear() => _collection.Clear(); + + bool ICollection.Contains(string item) => _collection.Contains(item); + + void ICollection.CopyTo(string[] array, int arrayIndex) => _collection.CopyTo(array, arrayIndex); + + IEnumerator IEnumerable.GetEnumerator() => _collection.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_collection).GetEnumerator(); + + bool ICollection.Remove(string item) => _collection.Remove(item); + } } """;