From 2c89987191f90199f331a8d0e4e86f3dde054aed Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Wed, 31 Jul 2024 12:45:20 -0700 Subject: [PATCH 1/3] Make Config generator test for downcasts We were hitting compiler errors when the generator emitted test casts for value types. Since those can never be true (value types cannot be derived from, and the compiler can see if the cast will succeed or not). Fix this by only doing a test cast when we are trying to do a runtime downcast. --- .../ConfigurationBindingGenerator.Parser.cs | 16 ++++ .../gen/Emitter/CoreBindingHelpers.cs | 24 ++++-- .../gen/Specs/Types/CollectionSpec.cs | 2 + .../ConfigurationBinderTests.Collections.cs | 52 +++++++++++++ ...tionBinderTests.TestClasses.Collections.cs | 74 +++++++++++++++++++ .../net462/Version0/Collections.generated.txt | 26 ++++++- .../net462/Version1/Collections.generated.txt | 26 ++++++- .../Version0/Collections.generated.txt | 26 ++++++- .../Version1/Collections.generated.txt | 26 ++++++- .../GeneratorTests.Baselines.cs | 28 +++++++ 10 files changed, 285 insertions(+), 15 deletions(-) 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..daf9fbaf0bbd73 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 shouldTestCast = 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; + shouldTestCast = 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(!shouldTestCast || !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, + ShouldTestCast = shouldTestCast }; } @@ -458,6 +464,7 @@ private TypeSpec CreateEnumerableSpec(TypeParseInfo typeParseInfo) CollectionInstantiationStrategy instantiationStrategy; CollectionInstantiationConcreteType instantiationConcreteType; CollectionPopulationCastType populationCastType; + bool shouldTestCast = 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; + shouldTestCast = 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; + shouldTestCast = 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; + shouldTestCast = true; } else { @@ -514,12 +528,14 @@ private TypeSpec CreateEnumerableSpec(TypeParseInfo typeParseInfo) TypeRef elementTypeRef = EnqueueTransitiveType(typeParseInfo, elementType, DiagnosticDescriptors.ElementTypeNotSupported); + Debug.Assert(!shouldTestCast || !type.IsValueType, "Should not test cast for value types."); return new EnumerableSpec(type) { ElementTypeRef = elementTypeRef, InstantiationStrategy = instantiationStrategy, InstantiationConcreteType = instantiationConcreteType, PopulationCastType = populationCastType, + ShouldTestCast = shouldTestCast }; } 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..2077910e75c2f9 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.ShouldTestCast) + { + _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..16ee353d3333b6 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 ShouldTestCast {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..3a413056c22911 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,58 @@ 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"]); + } + // 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..36b86adda34624 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,78 @@ 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); + } } } 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); + } } """; From 847736f9e346f5c615871263cf293a007f9724af Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Thu, 8 Aug 2024 13:57:37 -0700 Subject: [PATCH 2/3] Rename ShouldTestCast -> ShouldTryCast --- .../ConfigurationBindingGenerator.Parser.cs | 20 +++++++++---------- .../gen/Emitter/CoreBindingHelpers.cs | 2 +- .../gen/Specs/Types/CollectionSpec.cs | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) 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 daf9fbaf0bbd73..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,7 +391,7 @@ private TypeSpec CreateDictionarySpec(TypeParseInfo typeParseInfo, ITypeSymbol k CollectionInstantiationStrategy instantiationStrategy; CollectionInstantiationConcreteType instantiationConcreteType; CollectionPopulationCastType populationCastType; - bool shouldTestCast = false; + bool shouldTryCast = false; if (HasPublicParameterLessCtor(type)) { @@ -425,7 +425,7 @@ private TypeSpec CreateDictionarySpec(TypeParseInfo typeParseInfo, ITypeSymbol k instantiationConcreteType = CollectionInstantiationConcreteType.Dictionary; // is IReadonlyDictionary<,> -- test cast to IDictionary<,> populationCastType = CollectionPopulationCastType.IDictionary; - shouldTestCast = true; + shouldTryCast = true; } else { @@ -435,7 +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(!shouldTestCast || !type.IsValueType, "Should not test cast for value types."); + Debug.Assert(!shouldTryCast || !type.IsValueType, "Should not test cast for value types."); return new DictionarySpec(type) { KeyTypeRef = keyTypeRef, @@ -443,7 +443,7 @@ private TypeSpec CreateDictionarySpec(TypeParseInfo typeParseInfo, ITypeSymbol k InstantiationStrategy = instantiationStrategy, InstantiationConcreteType = instantiationConcreteType, PopulationCastType = populationCastType, - ShouldTestCast = shouldTestCast + ShouldTryCast = shouldTryCast }; } @@ -464,7 +464,7 @@ private TypeSpec CreateEnumerableSpec(TypeParseInfo typeParseInfo) CollectionInstantiationStrategy instantiationStrategy; CollectionInstantiationConcreteType instantiationConcreteType; CollectionPopulationCastType populationCastType; - bool shouldTestCast = false; + bool shouldTryCast = false; if (HasPublicParameterLessCtor(type)) { @@ -497,7 +497,7 @@ private TypeSpec CreateEnumerableSpec(TypeParseInfo typeParseInfo) instantiationConcreteType = CollectionInstantiationConcreteType.List; // is IEnumerable<> -- test cast to ICollection<> populationCastType = CollectionPopulationCastType.ICollection; - shouldTestCast = true; + shouldTryCast = true; } else if (IsInterfaceMatch(type, _typeSymbols.ISet_Unbound)) { @@ -511,7 +511,7 @@ private TypeSpec CreateEnumerableSpec(TypeParseInfo typeParseInfo) instantiationConcreteType = CollectionInstantiationConcreteType.HashSet; // is IReadOnlySet<> -- test cast to ISet<> populationCastType = CollectionPopulationCastType.ISet; - shouldTestCast = true; + shouldTryCast = true; } else if (IsInterfaceMatch(type, _typeSymbols.IReadOnlyList_Unbound) || IsInterfaceMatch(type, _typeSymbols.IReadOnlyCollection_Unbound)) { @@ -519,7 +519,7 @@ private TypeSpec CreateEnumerableSpec(TypeParseInfo typeParseInfo) instantiationConcreteType = CollectionInstantiationConcreteType.List; // is IReadOnlyList<> or IReadOnlyCollection<> -- test cast to ICollection<> populationCastType = CollectionPopulationCastType.ICollection; - shouldTestCast = true; + shouldTryCast = true; } else { @@ -528,14 +528,14 @@ private TypeSpec CreateEnumerableSpec(TypeParseInfo typeParseInfo) TypeRef elementTypeRef = EnqueueTransitiveType(typeParseInfo, elementType, DiagnosticDescriptors.ElementTypeNotSupported); - Debug.Assert(!shouldTestCast || !type.IsValueType, "Should not test cast for value types."); + Debug.Assert(!shouldTryCast || !type.IsValueType, "Should not test cast for value types."); return new EnumerableSpec(type) { ElementTypeRef = elementTypeRef, InstantiationStrategy = instantiationStrategy, InstantiationConcreteType = instantiationConcreteType, PopulationCastType = populationCastType, - ShouldTestCast = shouldTestCast + 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 2077910e75c2f9..3433544bf9ee74 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs @@ -1233,7 +1233,7 @@ private void EmitCollectionCastIfRequired(CollectionWithCtorInitSpec type, out s instanceIdentifier = Identifier.temp; string castExpression = $"{TypeIndex.GetPopulationCastTypeDisplayString(type)} {instanceIdentifier}"; - if (type.ShouldTestCast) + if (type.ShouldTryCast) { _writer.WriteLine($$""" if ({{Identifier.instance}} is not {{castExpression}}) 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 16ee353d3333b6..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 @@ -24,7 +24,7 @@ protected CollectionWithCtorInitSpec(ITypeSymbol type) : base(type) { } public required CollectionPopulationCastType PopulationCastType { get; init; } - public required bool ShouldTestCast {get; init; } + public required bool ShouldTryCast { get; init; } } internal sealed record ArraySpec : CollectionSpec From be49e4f2769ea72fb7835731b4d0287b7a6bdc3f Mon Sep 17 00:00:00 2001 From: Eric StJohn Date: Thu, 8 Aug 2024 13:59:09 -0700 Subject: [PATCH 3/3] Add tests for readonly collection structs These are unsupported and ignored by the generator and a diagnostic is emitted. Binding data to them does nothing. --- .../ConfigurationBinderTests.Collections.cs | 25 +++++++++++ ...tionBinderTests.TestClasses.Collections.cs | 45 +++++++++++++++++++ 2 files changed, 70 insertions(+) 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 3a413056c22911..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 @@ -2420,6 +2420,31 @@ public void TestOptionsWithStructs() 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 36b86adda34624..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 @@ -462,5 +462,50 @@ public DictionaryStructExplicit() {} 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); + } } }