From 871166ce3060edaeeef69e4c4a10084429df017c Mon Sep 17 00:00:00 2001 From: stevedunnhq Date: Sun, 17 Apr 2022 18:31:49 +0100 Subject: [PATCH 01/28] First pass --- .../tests/ConfigurationBinderTests.cs | 116 +++++++++++++++++- .../ConfigurationCollectionBindingTests.cs | 63 ++++++++++ 2 files changed, 178 insertions(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index b7795c4ccf42c2..4a46e811cde8ff 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -49,7 +49,13 @@ public string ReadOnly get { return null; } } + public ISet NonInstantiatedISet { get; set; } = null!; + public IDictionary> NonInstantiatedDictionaryWithISet { get; set; } = null!; + public IDictionary> InstantiatedDictionaryWithHashSet { get; set; } = + new Dictionary>(); + public IEnumerable NonInstantiatedIEnumerable { get; set; } = null!; + public ISet InstantiatedISet { get; set; } = new HashSet(); public IEnumerable InstantiatedIEnumerable { get; set; } = new List(); public ICollection InstantiatedICollection { get; set; } = new List(); public IReadOnlyCollection InstantiatedIReadOnlyCollection { get; set; } = new List(); @@ -478,6 +484,114 @@ public void CanBindNonInstantiatedIEnumerableWithItems() Assert.Equal("Yo2", options.NonInstantiatedIEnumerable.ElementAt(1)); } + [Fact] + public void CanBindNonInstantiatedISet() + { + var dic = new Dictionary + { + {"NonInstantiatedISet:0", "Yo1"}, + {"NonInstantiatedISet:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.NonInstantiatedISet.Count()); + Assert.Equal("Yo1", options.NonInstantiatedISet.ElementAt(0)); + Assert.Equal("Yo2", options.NonInstantiatedISet.ElementAt(1)); + } + + [Fact] + public void CanBindNonInstantiatedDictionaryOfISet() + { + var dic = new Dictionary + { + {"NonInstantiatedDictionaryWithISet:foo:0", "foo-1"}, + {"NonInstantiatedDictionaryWithISet:foo:1", "foo-2"}, + {"NonInstantiatedDictionaryWithISet:bar:0", "bar-1"}, + {"NonInstantiatedDictionaryWithISet:bar:1", "bar-2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.NonInstantiatedDictionaryWithISet.Count); + Assert.Equal("foo-1", options.NonInstantiatedDictionaryWithISet["foo"].ElementAt(0)); + Assert.Equal("foo-2", options.NonInstantiatedDictionaryWithISet["foo"].ElementAt(1)); + Assert.Equal("bar-1", options.NonInstantiatedDictionaryWithISet["bar"].ElementAt(0)); + Assert.Equal("bar-2", options.NonInstantiatedDictionaryWithISet["bar"].ElementAt(1)); + } + + [Fact] + public void CanBindInstantiatedDictionaryOfISet() + { + var dic = new Dictionary + { + {"InstantiatedDictionaryWithHashSet:foo:0", "foo-1"}, + {"InstantiatedDictionaryWithHashSet:foo:1", "foo-2"}, + {"InstantiatedDictionaryWithHashSet:bar:0", "bar-1"}, + {"InstantiatedDictionaryWithHashSet:bar:1", "bar-2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.InstantiatedDictionaryWithHashSet.Count); + Assert.Equal("foo-1", options.InstantiatedDictionaryWithHashSet["foo"].ElementAt(0)); + Assert.Equal("foo-2", options.InstantiatedDictionaryWithHashSet["foo"].ElementAt(1)); + Assert.Equal("bar-1", options.InstantiatedDictionaryWithHashSet["bar"].ElementAt(0)); + Assert.Equal("bar-2", options.InstantiatedDictionaryWithHashSet["bar"].ElementAt(1)); + } + + [Fact] + public void CanBindInstantiatedISet() + { + var dic = new Dictionary + { + {"InstantiatedISet:0", "Yo1"}, + {"InstantiatedISet:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.InstantiatedISet.Count()); + Assert.Equal("Yo1", options.InstantiatedISet.ElementAt(0)); + Assert.Equal("Yo2", options.InstantiatedISet.ElementAt(1)); + } + + // [Fact] + // public void CanBindInstantiatedISet2() + // { + // var dic = new Dictionary + // { + // {"NonInstantiatedISet:0", "Yo1"}, + // {"NonInstantiatedISet:1", "Yo2"}, + // }; + // var configurationBuilder = new ConfigurationBuilder(); + // configurationBuilder.AddInMemoryCollection(dic); + // + // var config = configurationBuilder.Build(); + // + // var options = config.Get()!; + // + // Assert.Equal(2, options.NonInstantiatedISet.Count()); + // Assert.Equal("Yo1", options.NonInstantiatedISet.ElementAt(0)); + // Assert.Equal("Yo2", options.NonInstantiatedISet.ElementAt(1)); + // } + [Fact] public void CanBindInstantiatedIEnumerableWithItems() { @@ -1107,7 +1221,7 @@ public void BindCanSetNonPublicWhenSet(string property) var config = configurationBuilder.Build(); var options = new ComplexOptions(); - config.Bind(options, o => o.BindNonPublicProperties = true ); + config.Bind(options, o => o.BindNonPublicProperties = true); Assert.Equal("stuff", options.GetType().GetTypeInfo().GetDeclaredProperty(property).GetValue(options)); } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs index 8e4b9bea54a82c..d6c5dbce59045e 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs @@ -581,6 +581,32 @@ public void AlreadyInitializedStringDictionaryBinding() Assert.Equal("val_3", options.AlreadyInitializedStringDictionaryInterface["ghi"]); } + [Fact] + public void AlreadyInitializedHashSetDictionaryBinding() + { + var input = new Dictionary + { + {"AlreadyInitializedHashSetDictionary:123:0", "val_1"}, + {"AlreadyInitializedHashSetDictionary:123:1", "val_2"}, + {"AlreadyInitializedHashSetDictionary:123:2", "val_3"} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + var options = new OptionsWithDictionary(); + config.Bind(options); + + Assert.NotNull(options.AlreadyInitializedHashSetDictionary); + Assert.Equal(1, options.AlreadyInitializedHashSetDictionary.Count); + + Assert.Equal("This was already here", options.AlreadyInitializedHashSetDictionary["123"].ElementAt(0)); + Assert.Equal("val_1", options.AlreadyInitializedHashSetDictionary["123"].ElementAt(1)); + Assert.Equal("val_2", options.AlreadyInitializedHashSetDictionary["123"].ElementAt(2)); + Assert.Equal("val_3", options.AlreadyInitializedHashSetDictionary["123"].ElementAt(3)); + } + [Fact] public void CanOverrideExistingDictionaryKey() { @@ -684,6 +710,36 @@ public void ListDictionary() Assert.Equal("def_2", options.ListDictionary["def"][2]); } + [Fact] + public void ISetDictionary() + { + var input = new Dictionary + { + {"ISetDictionary:abc:0", "abc_0"}, + {"ISetDictionary:abc:1", "abc_1"}, + {"ISetDictionary:def:0", "def_0"}, + {"ISetDictionary:def:1", "def_1"}, + {"ISetDictionary:def:2", "def_2"} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + var options = new OptionsWithDictionary(); + config.Bind(options); + + Assert.Equal(2, options.ISetDictionary.Count); + Assert.Equal(2, options.ISetDictionary["abc"].Count); + Assert.Equal(3, options.ISetDictionary["def"].Count); + + Assert.Equal("abc_0", options.ISetDictionary["abc"].ElementAt(0)); + Assert.Equal("abc_1", options.ISetDictionary["abc"].ElementAt(1)); + Assert.Equal("def_0", options.ISetDictionary["def"].ElementAt(0)); + Assert.Equal("def_1", options.ISetDictionary["def"].ElementAt(1)); + Assert.Equal("def_2", options.ISetDictionary["def"].ElementAt(2)); + } + [Fact] public void ListInNestedOptionBinding() { @@ -1410,6 +1466,11 @@ public OptionsWithDictionary() { ["123"] = "This was already here" }; + + AlreadyInitializedHashSetDictionary = new Dictionary> + { + ["123"] = new HashSet(new[] {"This was already here"}) + }; } public Dictionary IntDictionary { get; set; } @@ -1418,6 +1479,7 @@ public OptionsWithDictionary() public Dictionary ObjectDictionary { get; set; } + public Dictionary> ISetDictionary { get; set; } public Dictionary> ListDictionary { get; set; } public Dictionary NonStringKeyDictionary { get; set; } @@ -1427,6 +1489,7 @@ public OptionsWithDictionary() public IDictionary StringDictionaryInterface { get; set; } public IDictionary AlreadyInitializedStringDictionaryInterface { get; set; } + public IDictionary> AlreadyInitializedHashSetDictionary { get; set; } } private class OptionsWithInterdependentProperties From 3fe871440634ee8f91834569d7a4ffc338270d87 Mon Sep 17 00:00:00 2001 From: stevedunnhq Date: Sun, 17 Apr 2022 18:37:59 +0100 Subject: [PATCH 02/28] Remove commented code --- .../tests/ConfigurationBinderTests.cs | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 4a46e811cde8ff..4407e0d4e51534 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -572,26 +572,6 @@ public void CanBindInstantiatedISet() Assert.Equal("Yo2", options.InstantiatedISet.ElementAt(1)); } - // [Fact] - // public void CanBindInstantiatedISet2() - // { - // var dic = new Dictionary - // { - // {"NonInstantiatedISet:0", "Yo1"}, - // {"NonInstantiatedISet:1", "Yo2"}, - // }; - // var configurationBuilder = new ConfigurationBuilder(); - // configurationBuilder.AddInMemoryCollection(dic); - // - // var config = configurationBuilder.Build(); - // - // var options = config.Get()!; - // - // Assert.Equal(2, options.NonInstantiatedISet.Count()); - // Assert.Equal("Yo1", options.NonInstantiatedISet.ElementAt(0)); - // Assert.Equal("Yo2", options.NonInstantiatedISet.ElementAt(1)); - // } - [Fact] public void CanBindInstantiatedIEnumerableWithItems() { From 65ea98924106d8c5f2925a1307464bd4d3bb279a Mon Sep 17 00:00:00 2001 From: stevedunnhq Date: Mon, 18 Apr 2022 08:17:30 +0100 Subject: [PATCH 03/28] Support IReadOnlySet. Refactor out method that determines most suitable type for collections that are already specified in the destination. --- .../src/ConfigurationBinder.cs | 16 +++++-- .../tests/ConfigurationBinderTests.cs | 47 ++++++++++++++++++- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 7eca0e5f41db86..57bd8a96f790ac 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -303,11 +304,14 @@ private static object BindToCollection(Type type, IConfiguration config, BinderO return instance; } - collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyCollection<>), type); + collectionInterface = FindOpenGenericInterface(typeof(ISet<>), type); +#if NET5_0_OR_GREATER + collectionInterface ??= FindOpenGenericInterface(typeof(IReadOnlySet<>), type); +#endif if (collectionInterface != null) { - // IReadOnlyCollection is guaranteed to have exactly one parameter - return BindToCollection(type, config, options); + // ISet is guaranteed to have exactly one parameter + return BindToSet(type, config, options); } collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type); @@ -615,7 +619,11 @@ private static Array BindArray(Type type, IEnumerable? source, IConfiguration co else // e. g. IEnumerable { elementType = type.GetGenericArguments()[0]; - } + + if ((type = sourceType.GetInterface("IReadOnlySet`1", false)) != null) + { + return (typeof(HashSet<>), type.GenericTypeArguments[0]); + } IList list = new List(); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 4407e0d4e51534..789be41819ac48 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -56,6 +56,10 @@ public string ReadOnly public IEnumerable NonInstantiatedIEnumerable { get; set; } = null!; public ISet InstantiatedISet { get; set; } = new HashSet(); + #if NET5_0_OR_GREATER + public IReadOnlySet InstantiatedIReadOnlySet { get; set; } = new HashSet(); + public IReadOnlySet NonInstantiatedIReadOnlySet { get; set; } + #endif public IEnumerable InstantiatedIEnumerable { get; set; } = new List(); public ICollection InstantiatedICollection { get; set; } = new List(); public IReadOnlyCollection InstantiatedIReadOnlyCollection { get; set; } = new List(); @@ -499,11 +503,52 @@ public void CanBindNonInstantiatedISet() var options = config.Get()!; - Assert.Equal(2, options.NonInstantiatedISet.Count()); + Assert.Equal(2, options.NonInstantiatedISet.Count); Assert.Equal("Yo1", options.NonInstantiatedISet.ElementAt(0)); Assert.Equal("Yo2", options.NonInstantiatedISet.ElementAt(1)); } +#if NET5_0_OR_GREATER + [Fact] + public void CanBindInstantiatedIReadOnlySet() + { + var dic = new Dictionary + { + {"InstantiatedIReadOnlySet:0", "Yo1"}, + {"InstantiatedIReadOnlySet:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.InstantiatedIReadOnlySet.Count); + Assert.Equal("Yo1", options.InstantiatedIReadOnlySet.ElementAt(0)); + Assert.Equal("Yo2", options.InstantiatedIReadOnlySet.ElementAt(1)); + } + + [Fact] + public void CanBindNonInstantiatedIReadOnlySet() + { + var dic = new Dictionary + { + {"NonInstantiatedIReadOnlySet:0", "Yo1"}, + {"NonInstantiatedIReadOnlySet:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.NonInstantiatedIReadOnlySet.Count); + Assert.Equal("Yo1", options.NonInstantiatedIReadOnlySet.ElementAt(0)); + Assert.Equal("Yo2", options.NonInstantiatedIReadOnlySet.ElementAt(1)); + } +#endif [Fact] public void CanBindNonInstantiatedDictionaryOfISet() { From c2d10381f8fd690759dec0d67689bc9e40272e9e Mon Sep 17 00:00:00 2001 From: stevedunnhq Date: Tue, 19 Apr 2022 07:14:20 +0100 Subject: [PATCH 04/28] Handles ISet after rebase. Still to do IReadOnlySet --- .../src/ConfigurationBinder.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 57bd8a96f790ac..94bf95dbf64881 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -4,7 +4,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -259,6 +258,15 @@ private static void BindProperty(PropertyInfo property, object instance, IConfig } } + [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the object collection in type so its members may be trimmed.")] + private static object BindToSet(Type type, IConfiguration config, BinderOptions options) + { + Type genericType = typeof(HashSet<>).MakeGenericType(type.GenericTypeArguments[0]); + object instance = Activator.CreateInstance(genericType)!; + BindCollection(instance, genericType, config, options); + return instance; + } + [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the object collection in type so its members may be trimmed.")] private static object BindToCollection(Type type, IConfiguration config, BinderOptions options) { @@ -314,6 +322,13 @@ private static object BindToCollection(Type type, IConfiguration config, BinderO return BindToSet(type, config, options); } + collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyCollection<>), type); + if (collectionInterface != null) + { + // IReadOnlyCollection is guaranteed to have exactly one parameter + return BindToCollection(type, config, options); + } + collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type); if (collectionInterface != null) { @@ -619,11 +634,7 @@ private static Array BindArray(Type type, IEnumerable? source, IConfiguration co else // e. g. IEnumerable { elementType = type.GetGenericArguments()[0]; - - if ((type = sourceType.GetInterface("IReadOnlySet`1", false)) != null) - { - return (typeof(HashSet<>), type.GenericTypeArguments[0]); - } + } IList list = new List(); From 2613c44b11375d5267d0b1c9656d008f1ccc9d64 Mon Sep 17 00:00:00 2001 From: stevedunnhq Date: Tue, 19 Apr 2022 12:03:39 +0100 Subject: [PATCH 05/28] Add support back for `IReadOnlySet` --- .../src/ConfigurationBinder.cs | 63 +++++++++ .../tests/ConfigurationBinderTests.cs | 120 +++++++++++++++++- 2 files changed, 181 insertions(+), 2 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 94bf95dbf64881..a168ca9b92e9a1 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -386,6 +386,16 @@ private static void BindInstance( return; } + // for sets and read-only set interfaces, we concatenate on to what is already there + if (IsSet(type)) + { + if (!bindingPoint.IsReadOnly) + { + bindingPoint.SetValue(BindSet(type, (IEnumerable?)bindingPoint.Value, config, options)); + } + return; + } + // If we don't have an instance, try to create one if (bindingPoint.Value is null) { @@ -671,6 +681,47 @@ private static Array BindArray(Type type, IEnumerable? source, IConfiguration co return result; } + [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the Array so its members may be trimmed.")] + private static object BindSet(Type type, IEnumerable? source, IConfiguration config, BinderOptions options) + { + Type elementType = type.GetGenericArguments()[0]; + + Type genericType = typeof(HashSet<>).MakeGenericType(type.GenericTypeArguments[0]); + object instance = Activator.CreateInstance(genericType)!; + + MethodInfo addMethod = genericType.GetMethod("Add", DeclaredOnlyLookup)!; + + if (source != null) + { + foreach (object? item in source) + { + addMethod.Invoke(instance, new[] {item}); + } + } + + foreach (IConfigurationSection section in config.GetChildren()) + { + var itemBindingPoint = new BindingPoint(); + try + { + BindInstance( + type: elementType, + bindingPoint: itemBindingPoint, + config: section, + options: options); + if (itemBindingPoint.HasNewValue) + { + addMethod.Invoke(instance, new[] {itemBindingPoint.Value}); + } + } + catch + { + } + } + + return instance; + } + [RequiresUnreferencedCode(TrimmingWarningMessage)] private static bool TryConvertValue( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] @@ -748,6 +799,18 @@ private static bool IsArrayCompatibleReadOnlyInterface(Type type) || genericTypeDefinition == typeof(IReadOnlyList<>); } + private static bool IsSet(Type type) + { + if (!type.IsInterface || !type.IsConstructedGenericType) { return false; } + + Type genericTypeDefinition = type.GetGenericTypeDefinition(); + return genericTypeDefinition == typeof(ISet<>) +#if NET5_0_OR_GREATER + || genericTypeDefinition == typeof(IReadOnlySet<>) +#endif + ; + } + private static Type? FindOpenGenericInterface( Type expected, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 789be41819ac48..8e0e4259b34bea 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.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.ComponentModel; using System.Linq; @@ -53,13 +54,29 @@ public string ReadOnly public IDictionary> NonInstantiatedDictionaryWithISet { get; set; } = null!; public IDictionary> InstantiatedDictionaryWithHashSet { get; set; } = new Dictionary>(); + + public IDictionary> InstantiatedDictionaryWithHashSetWithSomeValues { get; set; } = + new Dictionary> + { + {"item1", new HashSet(new[] {"existing1", "existing2"})} + }; public IEnumerable NonInstantiatedIEnumerable { get; set; } = null!; public ISet InstantiatedISet { get; set; } = new HashSet(); - #if NET5_0_OR_GREATER + public ISet InstantiatedISetWithSomeValues { get; set; } = + new HashSet(new[] {"existing1", "existing2"}); + +#if NET5_0_OR_GREATER public IReadOnlySet InstantiatedIReadOnlySet { get; set; } = new HashSet(); + public IReadOnlySet InstantiatedIReadOnlySetWithSomeValues { get; set; } = + new HashSet(new[] {"existing1", "existing2"}); public IReadOnlySet NonInstantiatedIReadOnlySet { get; set; } - #endif + public IDictionary> InstantiatedDictionaryWithReadOnlySetWithSomeValues { get; set; } = + new Dictionary> + { + {"item1", new HashSet(new[] {"existing1", "existing2"})} + }; +#endif public IEnumerable InstantiatedIEnumerable { get; set; } = new List(); public ICollection InstantiatedICollection { get; set; } = new List(); public IReadOnlyCollection InstantiatedIReadOnlyCollection { get; set; } = new List(); @@ -529,6 +546,28 @@ public void CanBindInstantiatedIReadOnlySet() Assert.Equal("Yo2", options.InstantiatedIReadOnlySet.ElementAt(1)); } + [Fact] + public void CanBindInstantiatedIReadOnlyWithSomeValues() + { + var dic = new Dictionary + { + {"InstantiatedIReadOnlySetWithSomeValues:0", "Yo1"}, + {"InstantiatedIReadOnlySetWithSomeValues:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(4, options.InstantiatedIReadOnlySetWithSomeValues.Count); + Assert.Equal("existing1", options.InstantiatedIReadOnlySetWithSomeValues.ElementAt(0)); + Assert.Equal("existing2", options.InstantiatedIReadOnlySetWithSomeValues.ElementAt(1)); + Assert.Equal("Yo1", options.InstantiatedIReadOnlySetWithSomeValues.ElementAt(2)); + Assert.Equal("Yo2", options.InstantiatedIReadOnlySetWithSomeValues.ElementAt(3)); + } + [Fact] public void CanBindNonInstantiatedIReadOnlySet() { @@ -548,7 +587,35 @@ public void CanBindNonInstantiatedIReadOnlySet() Assert.Equal("Yo1", options.NonInstantiatedIReadOnlySet.ElementAt(0)); Assert.Equal("Yo2", options.NonInstantiatedIReadOnlySet.ElementAt(1)); } + + [Fact] + public void CanBindInstantiatedDictionaryOfIReadOnlySetWithSomeExistingValues() + { + var dic = new Dictionary + { + {"InstantiatedDictionaryWithReadOnlySetWithSomeValues:foo:0", "foo-1"}, + {"InstantiatedDictionaryWithReadOnlySetWithSomeValues:foo:1", "foo-2"}, + {"InstantiatedDictionaryWithReadOnlySetWithSomeValues:bar:0", "bar-1"}, + {"InstantiatedDictionaryWithReadOnlySetWithSomeValues:bar:1", "bar-2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(3, options.InstantiatedDictionaryWithReadOnlySetWithSomeValues.Count); + Assert.Equal("existing1", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["item1"].ElementAt(0)); + Assert.Equal("existing2", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["item1"].ElementAt(1)); + + Assert.Equal("foo-1", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["foo"].ElementAt(0)); + Assert.Equal("foo-2", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["foo"].ElementAt(1)); + Assert.Equal("bar-1", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["bar"].ElementAt(0)); + Assert.Equal("bar-2", options.InstantiatedDictionaryWithReadOnlySetWithSomeValues["bar"].ElementAt(1)); + } #endif + [Fact] public void CanBindNonInstantiatedDictionaryOfISet() { @@ -597,6 +664,33 @@ public void CanBindInstantiatedDictionaryOfISet() Assert.Equal("bar-2", options.InstantiatedDictionaryWithHashSet["bar"].ElementAt(1)); } + [Fact] + public void CanBindInstantiatedDictionaryOfISetWithSomeExistingValues() + { + var dic = new Dictionary + { + {"InstantiatedDictionaryWithHashSetWithSomeValues:foo:0", "foo-1"}, + {"InstantiatedDictionaryWithHashSetWithSomeValues:foo:1", "foo-2"}, + {"InstantiatedDictionaryWithHashSetWithSomeValues:bar:0", "bar-1"}, + {"InstantiatedDictionaryWithHashSetWithSomeValues:bar:1", "bar-2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(3, options.InstantiatedDictionaryWithHashSetWithSomeValues.Count); + Assert.Equal("existing1", options.InstantiatedDictionaryWithHashSetWithSomeValues["item1"].ElementAt(0)); + Assert.Equal("existing2", options.InstantiatedDictionaryWithHashSetWithSomeValues["item1"].ElementAt(1)); + + Assert.Equal("foo-1", options.InstantiatedDictionaryWithHashSetWithSomeValues["foo"].ElementAt(0)); + Assert.Equal("foo-2", options.InstantiatedDictionaryWithHashSetWithSomeValues["foo"].ElementAt(1)); + Assert.Equal("bar-1", options.InstantiatedDictionaryWithHashSetWithSomeValues["bar"].ElementAt(0)); + Assert.Equal("bar-2", options.InstantiatedDictionaryWithHashSetWithSomeValues["bar"].ElementAt(1)); + } + [Fact] public void CanBindInstantiatedISet() { @@ -617,6 +711,28 @@ public void CanBindInstantiatedISet() Assert.Equal("Yo2", options.InstantiatedISet.ElementAt(1)); } + [Fact] + public void CanBindInstantiatedISetWithSomeValues() + { + var dic = new Dictionary + { + {"InstantiatedISetWithSomeValues:0", "Yo1"}, + {"InstantiatedISetWithSomeValues:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(4, options.InstantiatedISetWithSomeValues.Count); + Assert.Equal("existing1", options.InstantiatedISetWithSomeValues.ElementAt(0)); + Assert.Equal("existing2", options.InstantiatedISetWithSomeValues.ElementAt(1)); + Assert.Equal("Yo1", options.InstantiatedISetWithSomeValues.ElementAt(2)); + Assert.Equal("Yo2", options.InstantiatedISetWithSomeValues.ElementAt(3)); + } + [Fact] public void CanBindInstantiatedIEnumerableWithItems() { From b377ac153f19e0e0b503c5b9e2087d48c2e4cc55 Mon Sep 17 00:00:00 2001 From: stevedunnhq Date: Tue, 19 Apr 2022 19:58:54 +0100 Subject: [PATCH 06/28] PR feedback - pre-allocate just one array instead of many allocations in a loop --- .../src/ConfigurationBinder.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index a168ca9b92e9a1..0c95a76526e90b 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -608,6 +608,8 @@ private static void BindCollection( Type itemType = collectionType.GenericTypeArguments[0]; MethodInfo? addMethod = collectionType.GetMethod("Add", DeclaredOnlyLookup); + var arguments = new object?[1]; + foreach (IConfigurationSection section in config.GetChildren()) { try @@ -620,7 +622,8 @@ private static void BindCollection( options: options); if (itemBindingPoint.HasNewValue) { - addMethod?.Invoke(collection, new[] { itemBindingPoint.Value }); + arguments[0] = itemBindingPoint.Value; + addMethod?.Invoke(collection, arguments); } } catch @@ -691,11 +694,14 @@ private static object BindSet(Type type, IEnumerable? source, IConfiguration con MethodInfo addMethod = genericType.GetMethod("Add", DeclaredOnlyLookup)!; + object?[] arguments = new object?[1]; + if (source != null) { foreach (object? item in source) { - addMethod.Invoke(instance, new[] {item}); + arguments[0] = item; + addMethod.Invoke(instance, arguments); } } @@ -711,7 +717,9 @@ private static object BindSet(Type type, IEnumerable? source, IConfiguration con options: options); if (itemBindingPoint.HasNewValue) { - addMethod.Invoke(instance, new[] {itemBindingPoint.Value}); + arguments[0] = itemBindingPoint.Value; + + addMethod.Invoke(instance, arguments); } } catch From 10e62386928bfac3c0009ffcdb5d117de533d6a7 Mon Sep 17 00:00:00 2001 From: stevedunnhq Date: Fri, 22 Apr 2022 22:02:31 +0100 Subject: [PATCH 07/28] All tests pass. --- .../src/ConfigurationBinder.cs | 107 +++++++++++++----- .../tests/ConfigurationBinderTests.cs | 22 ++++ .../ConfigurationCollectionBindingTests.cs | 1 + 3 files changed, 101 insertions(+), 29 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 0c95a76526e90b..18dc7b27393037 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -276,74 +276,96 @@ private static object BindToCollection(Type type, IConfiguration config, BinderO return instance; } + // Called when the binding point doesn't have a value. We need to determine the best type + // to use given just an interface. + // If there is no best type to create, for instance, the user provided a customer interface that is `IEnumerable<>`, + // then we return null. // Try to create an array/dictionary instance to back various collection interfaces [RequiresUnreferencedCode("In case type is a Dictionary, cannot statically analyze what the element type is of the value objects in the dictionary so its members may be trimmed.")] - private static object? AttemptBindToCollectionInterfaces( + private static (bool WasCollection, object? NewInstance) AttemptBindToCollectionInterfaces( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type, IConfiguration config, BinderOptions options) { if (!type.IsInterface) { - return null; + return (false, null); } - Type? collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyList<>), type); + Type? collectionInterface = FindOpenGenericInterface(type, typeof(IReadOnlyList<>)); if (collectionInterface != null) { // IEnumerable is guaranteed to have exactly one parameter - return BindToCollection(type, config, options); + return (true, BindToCollection(type, config, options)); } - collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyDictionary<,>), type); + collectionInterface = FindOpenGenericInterface(type, typeof(IReadOnlyDictionary<,>)); if (collectionInterface != null) { Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(type.GenericTypeArguments[0], type.GenericTypeArguments[1]); object instance = Activator.CreateInstance(dictionaryType)!; BindDictionary(instance, dictionaryType, config, options); - return instance; + return (true, instance); } - collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type); + collectionInterface = FindOpenGenericInterface(type, typeof(IDictionary<,>)); if (collectionInterface != null) { object instance = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(type.GenericTypeArguments[0], type.GenericTypeArguments[1]))!; BindDictionary(instance, collectionInterface, config, options); - return instance; + return (true, instance); } - collectionInterface = FindOpenGenericInterface(typeof(ISet<>), type); + collectionInterface = FindOpenGenericInterface(type, typeof(ISet<>)); #if NET5_0_OR_GREATER - collectionInterface ??= FindOpenGenericInterface(typeof(IReadOnlySet<>), type); + collectionInterface ??= FindOpenGenericInterface(type, typeof(IReadOnlySet<>)); #endif if (collectionInterface != null) { // ISet is guaranteed to have exactly one parameter - return BindToSet(type, config, options); + return (true, BindToSet(type, config, options)); } - collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyCollection<>), type); + collectionInterface = FindOpenGenericInterface(type, typeof(IReadOnlyCollection<>)); if (collectionInterface != null) { // IReadOnlyCollection is guaranteed to have exactly one parameter - return BindToCollection(type, config, options); + return (true, BindToCollection(type, config, options)); } - collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type); + collectionInterface = FindOpenGenericInterface(type, typeof(ICollection<>)); if (collectionInterface != null) { // ICollection is guaranteed to have exactly one parameter - return BindToCollection(type, config, options); + return (true, BindToCollection(type, config, options)); } - collectionInterface = FindOpenGenericInterface(typeof(IEnumerable<>), type); + // We have an interface, and it's null, so we only treat IEnumerable<> as the special case for a list. + // If we have a custom interface that derives from IEnumerable<>, we have no way of knowing what implementation + // to use, so return null. + collectionInterface = FindOpenGenericInterface(type, typeof(IEnumerable<>)); if (collectionInterface != null) { - // IEnumerable is guaranteed to have exactly one parameter - return BindToCollection(type, config, options); + // if it's *exactly* an IEnumerable<>, then treat it as a list + if (type == typeof(IEnumerable<>)) + { + return (true, BindToCollection(type, config, options)); + } + + // otherwise, we say it was a collection, but nothing we could instantiate. + return (true, null); } - return null; + // if we get to hear, we don't regard the interface as a collection + return (false, null); + // collectionInterface = type == typeof(IEnumerable<>) ? FindOpenGenericInterface(type, typeof(IEnumerable<>)); + // if (collectionInterface != null) + // { + // // IEnumerable is guaranteed to have exactly one parameter + // return BindToCollection(type, config, options); + // } + // + // return null; } [RequiresUnreferencedCode(TrimmingWarningMessage)] @@ -405,10 +427,13 @@ private static void BindInstance( return; } - object? boundFromInterface = AttemptBindToCollectionInterfaces(type, config, options); - if (boundFromInterface != null) + (bool wasInterface, object? instance) = AttemptBindToCollectionInterfaces(type, config, options); + if (wasInterface) { - bindingPoint.SetValue(boundFromInterface); + if (instance!= null) + { + bindingPoint.SetValue(instance); + } return; // We are already done if binding to a new collection instance worked } @@ -416,7 +441,7 @@ private static void BindInstance( } // See if it's a Dictionary - Type? collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type); + Type? collectionInterface = FindOpenGenericInterface(type, typeof(IDictionary<,>)); if (collectionInterface != null) { BindDictionary(bindingPoint.Value!, collectionInterface, config, options); @@ -424,10 +449,11 @@ private static void BindInstance( else { // See if it's an ICollection - collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type); + collectionInterface = FindOpenGenericInterface(type, typeof(ICollection<>)); if (collectionInterface != null) { - BindCollection(bindingPoint.Value!, collectionInterface, config, options); + BindCollection(bindingPoint.Value!, bindingPoint.Value!.GetType(), config, options); + //BindCollection(bindingPoint.Value!, collectionInterface, config, options); } // Something else else @@ -605,8 +631,10 @@ private static void BindCollection( IConfiguration config, BinderOptions options) { // ICollection is guaranteed to have exactly one parameter - Type itemType = collectionType.GenericTypeArguments[0]; - MethodInfo? addMethod = collectionType.GetMethod("Add", DeclaredOnlyLookup); + Type itemType = collectionType.GenericTypeArguments.Length == 0 ? typeof(object) : collectionType.GenericTypeArguments[0]; + + MethodInfo? addMethod = collectionType + .GetMethods(BindingFlags.Instance | BindingFlags.Public).Single(m => m.Name == "Add" && m.GetParameters().Length == 1); var arguments = new object?[1]; @@ -628,6 +656,7 @@ private static void BindCollection( } catch { + // Debug.Assert(false, "!! I added this - remove it!"); } } } @@ -820,9 +849,29 @@ private static bool IsSet(Type type) } private static Type? FindOpenGenericInterface( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type givenType, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type genericType) + { + var interfaceTypes = givenType.GetInterfaces(); + + foreach (var it in interfaceTypes) + { + if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType) + return givenType; + } + + if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) + return givenType; + + Type? baseType = givenType.BaseType; + if (baseType == null) return null; + + return FindOpenGenericInterface(baseType, genericType); + } + + private static Type? FindOpenGenericInterface2( Type expected, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] - Type actual) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type actual) { if (actual.IsGenericType && actual.GetGenericTypeDefinition() == expected) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 8e0e4259b34bea..2729392299dc4c 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -102,6 +102,15 @@ public override string Virtual } } + public interface ICustomCollection : IEnumerable + { + } + public class MyClassWithCustomCollection + { + public ICustomCollection CustomCollection { get; set; } + } + + public class NullableOptions { public bool? MyNullableBool { get; set; } @@ -691,6 +700,19 @@ public void CanBindInstantiatedDictionaryOfISetWithSomeExistingValues() Assert.Equal("bar-2", options.InstantiatedDictionaryWithHashSetWithSomeValues["bar"].ElementAt(1)); } + [Fact] + public void SkipsCustomCollection() + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection( new Dictionary + { + ["CustomCollection:0"] = "Yo!", + }); + var config = configurationBuilder.Build(); + var instance = config.Get()!; + Assert.Null(instance.CustomCollection); + } + [Fact] public void CanBindInstantiatedISet() { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs index d6c5dbce59045e..d0adc1fd04db94 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs @@ -406,6 +406,7 @@ public void CustomListBinding() Assert.Equal("valx", list[3]); } + [Fact] public void ObjectListBinding() { From c8e7b60bf0f5b40aca88378fd417d196cd6338a1 Mon Sep 17 00:00:00 2001 From: stevedunnhq Date: Sat, 23 Apr 2022 07:38:46 +0100 Subject: [PATCH 08/28] Tidy --- .../src/ConfigurationBinder.cs | 66 +++++++++---------- .../ConfigurationCollectionBindingTests.cs | 1 - 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 18dc7b27393037..3dca8a6c2ede31 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -278,9 +278,8 @@ private static object BindToCollection(Type type, IConfiguration config, BinderO // Called when the binding point doesn't have a value. We need to determine the best type // to use given just an interface. - // If there is no best type to create, for instance, the user provided a customer interface that is `IEnumerable<>`, + // If there is no best type to create, for instance, the user provided a custom interface that is `IEnumerable<>`, // then we return null. - // Try to create an array/dictionary instance to back various collection interfaces [RequiresUnreferencedCode("In case type is a Dictionary, cannot statically analyze what the element type is of the value objects in the dictionary so its members may be trimmed.")] private static (bool WasCollection, object? NewInstance) AttemptBindToCollectionInterfaces( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] @@ -356,16 +355,8 @@ private static (bool WasCollection, object? NewInstance) AttemptBindToCollection return (true, null); } - // if we get to hear, we don't regard the interface as a collection + // If we get to here, we don't regard the interface as a collection return (false, null); - // collectionInterface = type == typeof(IEnumerable<>) ? FindOpenGenericInterface(type, typeof(IEnumerable<>)); - // if (collectionInterface != null) - // { - // // IEnumerable is guaranteed to have exactly one parameter - // return BindToCollection(type, config, options); - // } - // - // return null; } [RequiresUnreferencedCode(TrimmingWarningMessage)] @@ -430,10 +421,11 @@ private static void BindInstance( (bool wasInterface, object? instance) = AttemptBindToCollectionInterfaces(type, config, options); if (wasInterface) { - if (instance!= null) + if (instance != null) { bindingPoint.SetValue(instance); } + return; // We are already done if binding to a new collection instance worked } @@ -634,9 +626,14 @@ private static void BindCollection( Type itemType = collectionType.GenericTypeArguments.Length == 0 ? typeof(object) : collectionType.GenericTypeArguments[0]; MethodInfo? addMethod = collectionType - .GetMethods(BindingFlags.Instance | BindingFlags.Public).Single(m => m.Name == "Add" && m.GetParameters().Length == 1); + .GetMethods(BindingFlags.Instance | BindingFlags.Public).SingleOrDefault(m => m.Name == "Add" && m.GetParameters().Length == 1); + + if (addMethod is null) + { + return; + } - var arguments = new object?[1]; + object?[] arguments = new object?[1]; foreach (IConfigurationSection section in config.GetChildren()) { @@ -656,7 +653,6 @@ private static void BindCollection( } catch { - // Debug.Assert(false, "!! I added this - remove it!"); } } } @@ -848,37 +844,37 @@ private static bool IsSet(Type type) ; } + // Determines if the type is descended from the desired type. + // IsAssignableFrom doesn't handle private static Type? FindOpenGenericInterface( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type givenType, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type genericType) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type desiredType) { - var interfaceTypes = givenType.GetInterfaces(); + Type[] interfaceTypes = type.GetInterfaces(); - foreach (var it in interfaceTypes) + foreach (Type it in interfaceTypes) { - if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType) - return givenType; + if (it.IsGenericType && it.GetGenericTypeDefinition() == desiredType) + { + return type; + } } - if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType) - return givenType; - - Type? baseType = givenType.BaseType; - if (baseType == null) return null; + if (type.IsGenericType && type.GetGenericTypeDefinition() == desiredType) + { + return type; + } - return FindOpenGenericInterface(baseType, genericType); - } + Type? baseType = type.BaseType; - private static Type? FindOpenGenericInterface2( - Type expected, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type actual) - { - if (actual.IsGenericType && - actual.GetGenericTypeDefinition() == expected) + if (baseType == null) { - return actual; + return null; } + return FindOpenGenericInterface(baseType, desiredType); + } + Type[] interfaces = actual.GetInterfaces(); foreach (Type interfaceType in interfaces) { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs index d0adc1fd04db94..d6c5dbce59045e 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs @@ -406,7 +406,6 @@ public void CustomListBinding() Assert.Equal("valx", list[3]); } - [Fact] public void ObjectListBinding() { From ef33f6fd0a5a3ea18f228510c663154d671d5188 Mon Sep 17 00:00:00 2001 From: stevedunnhq Date: Sat, 23 Apr 2022 08:08:22 +0100 Subject: [PATCH 09/28] Skips custom sets --- .../src/ConfigurationBinder.cs | 48 ++++++------------- .../tests/ConfigurationBinderTests.cs | 21 ++++++++ 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 3dca8a6c2ede31..7eae15726ed2a3 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -291,27 +291,12 @@ private static (bool WasCollection, object? NewInstance) AttemptBindToCollection return (false, null); } - Type? collectionInterface = FindOpenGenericInterface(type, typeof(IReadOnlyList<>)); - if (collectionInterface != null) - { - // IEnumerable is guaranteed to have exactly one parameter - return (true, BindToCollection(type, config, options)); - } - - collectionInterface = FindOpenGenericInterface(type, typeof(IReadOnlyDictionary<,>)); - if (collectionInterface != null) - { - Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(type.GenericTypeArguments[0], type.GenericTypeArguments[1]); - object instance = Activator.CreateInstance(dictionaryType)!; - BindDictionary(instance, dictionaryType, config, options); - return (true, instance); - } - - collectionInterface = FindOpenGenericInterface(type, typeof(IDictionary<,>)); + Type? collectionInterface = FindOpenGenericInterface(type, typeof(IReadOnlyDictionary<,>)) ?? + FindOpenGenericInterface(type, typeof(IDictionary<,>)); if (collectionInterface != null) { object instance = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(type.GenericTypeArguments[0], type.GenericTypeArguments[1]))!; - BindDictionary(instance, collectionInterface, config, options); + BindDictionary(instance, typeof(Dictionary<,>).MakeGenericType(type.GenericTypeArguments[0], type.GenericTypeArguments[1]), config, options); return (true, instance); } @@ -321,37 +306,35 @@ private static (bool WasCollection, object? NewInstance) AttemptBindToCollection #endif if (collectionInterface != null) { - // ISet is guaranteed to have exactly one parameter - return (true, BindToSet(type, config, options)); - } - - collectionInterface = FindOpenGenericInterface(type, typeof(IReadOnlyCollection<>)); - if (collectionInterface != null) - { - // IReadOnlyCollection is guaranteed to have exactly one parameter - return (true, BindToCollection(type, config, options)); + if (type == typeof(ISet<>)) + { + return (true, BindToSet(type, config, options)); + } + // I[ReadOnly]Set is guaranteed to have exactly one parameter + return (true, null); } - collectionInterface = FindOpenGenericInterface(type, typeof(ICollection<>)); + collectionInterface = FindOpenGenericInterface(type, typeof(IReadOnlyCollection<>)) ?? + FindOpenGenericInterface(type, typeof(ICollection<>)); if (collectionInterface != null) { - // ICollection is guaranteed to have exactly one parameter + // I[ReadOnly]Collection is guaranteed to have exactly one parameter return (true, BindToCollection(type, config, options)); } - // We have an interface, and it's null, so we only treat IEnumerable<> as the special case for a list. + // We have an interface, and it's null, so we only treat IEnumerable<> as the special case for a List. // If we have a custom interface that derives from IEnumerable<>, we have no way of knowing what implementation // to use, so return null. collectionInterface = FindOpenGenericInterface(type, typeof(IEnumerable<>)); if (collectionInterface != null) { - // if it's *exactly* an IEnumerable<>, then treat it as a list + // If it's *exactly* an IEnumerable<>, then treat it as a List if (type == typeof(IEnumerable<>)) { return (true, BindToCollection(type, config, options)); } - // otherwise, we say it was a collection, but nothing we could instantiate. + // Otherwise, we say it was a collection, but nothing we could instantiate. return (true, null); } @@ -845,7 +828,6 @@ private static bool IsSet(Type type) } // Determines if the type is descended from the desired type. - // IsAssignableFrom doesn't handle private static Type? FindOpenGenericInterface( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type desiredType) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 2729392299dc4c..7bc5c071ed2ee2 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -109,6 +109,14 @@ public class MyClassWithCustomCollection { public ICustomCollection CustomCollection { get; set; } } + + public interface ICustomSet : ISet + { + } + public class MyClassWithCustomSet + { + public ICustomSet CustomSet { get; set; } + } public class NullableOptions @@ -713,6 +721,19 @@ public void SkipsCustomCollection() Assert.Null(instance.CustomCollection); } + [Fact] + public void SkipsCustomSet() + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection( new Dictionary + { + ["CustomSet:0"] = "Yo!", + }); + var config = configurationBuilder.Build(); + var instance = config.Get()!; + Assert.Null(instance.CustomSet); + } + [Fact] public void CanBindInstantiatedISet() { From accbc131c231a40777d1cddb56b9c46e5342bc8b Mon Sep 17 00:00:00 2001 From: stevedunnhq Date: Sat, 23 Apr 2022 09:06:54 +0100 Subject: [PATCH 10/28] Strip out unused code --- .../src/ConfigurationBinder.cs | 75 +++++++++---------- .../tests/ConfigurationBinderTests.cs | 44 +++++++++++ 2 files changed, 79 insertions(+), 40 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 7eae15726ed2a3..0006bc7b80db5e 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -291,55 +291,31 @@ private static (bool WasCollection, object? NewInstance) AttemptBindToCollection return (false, null); } - Type? collectionInterface = FindOpenGenericInterface(type, typeof(IReadOnlyDictionary<,>)) ?? - FindOpenGenericInterface(type, typeof(IDictionary<,>)); - if (collectionInterface != null) + if (TypeCanBeAssignedADictionary(type)) { - object instance = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(type.GenericTypeArguments[0], type.GenericTypeArguments[1]))!; - BindDictionary(instance, typeof(Dictionary<,>).MakeGenericType(type.GenericTypeArguments[0], type.GenericTypeArguments[1]), config, options); + Type typeOfKey = type.GenericTypeArguments[0]; + Type typeOfValue = type.GenericTypeArguments[1]; + object instance = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(typeOfKey, typeOfValue))!; + BindDictionary(instance, typeof(Dictionary<,>).MakeGenericType(typeOfKey, typeOfValue), config, options); return (true, instance); } - collectionInterface = FindOpenGenericInterface(type, typeof(ISet<>)); -#if NET5_0_OR_GREATER - collectionInterface ??= FindOpenGenericInterface(type, typeof(IReadOnlySet<>)); -#endif - if (collectionInterface != null) + if (TypeCanBeAssignedAHashSet(type)) { - if (type == typeof(ISet<>)) - { - return (true, BindToSet(type, config, options)); - } - // I[ReadOnly]Set is guaranteed to have exactly one parameter - return (true, null); + return (true, BindToSet(type, config, options)); } - collectionInterface = FindOpenGenericInterface(type, typeof(IReadOnlyCollection<>)) ?? - FindOpenGenericInterface(type, typeof(ICollection<>)); - if (collectionInterface != null) + if (TypeCanBeAssignedAList(type)) { - // I[ReadOnly]Collection is guaranteed to have exactly one parameter return (true, BindToCollection(type, config, options)); } - // We have an interface, and it's null, so we only treat IEnumerable<> as the special case for a List. - // If we have a custom interface that derives from IEnumerable<>, we have no way of knowing what implementation - // to use, so return null. - collectionInterface = FindOpenGenericInterface(type, typeof(IEnumerable<>)); - if (collectionInterface != null) - { - // If it's *exactly* an IEnumerable<>, then treat it as a List - if (type == typeof(IEnumerable<>)) - { - return (true, BindToCollection(type, config, options)); - } - - // Otherwise, we say it was a collection, but nothing we could instantiate. - return (true, null); - } + // The interface that we've been given is not something that can be assigned a hashset, list, or dictionary. + // So all we need to return now is whether or not it was a IEnumerable. If it + // was a collection, the caller binds null to it, if it wasn't, the caller creates + // an instance of the type - // If we get to here, we don't regard the interface as a collection - return (false, null); + return (FindOpenGenericInterface(type, typeof(IEnumerable<>)) != null, null); } [RequiresUnreferencedCode(TrimmingWarningMessage)] @@ -383,7 +359,7 @@ private static void BindInstance( } // for sets and read-only set interfaces, we concatenate on to what is already there - if (IsSet(type)) + if (TypeCanBeAssignedAHashSet(type)) { if (!bindingPoint.IsReadOnly) { @@ -428,7 +404,6 @@ private static void BindInstance( if (collectionInterface != null) { BindCollection(bindingPoint.Value!, bindingPoint.Value!.GetType(), config, options); - //BindCollection(bindingPoint.Value!, collectionInterface, config, options); } // Something else else @@ -805,6 +780,26 @@ private static bool TryConvertValue( return result; } + private static bool TypeCanBeAssignedAList(Type type) + { + if (!type.IsInterface || !type.IsConstructedGenericType) { return false; } + + Type genericTypeDefinition = type.GetGenericTypeDefinition(); + return genericTypeDefinition == typeof(IEnumerable<>) + || genericTypeDefinition == typeof(IReadOnlyCollection<>) + || genericTypeDefinition == typeof(ICollection<>) + || genericTypeDefinition == typeof(IList<>) + || genericTypeDefinition == typeof(IReadOnlyList<>); + } + private static bool TypeCanBeAssignedADictionary(Type type) + { + if (!type.IsInterface || !type.IsConstructedGenericType) { return false; } + + Type genericTypeDefinition = type.GetGenericTypeDefinition(); + return genericTypeDefinition == typeof(IDictionary<,>) + || genericTypeDefinition == typeof(IReadOnlyDictionary<,>); + } + private static bool IsArrayCompatibleReadOnlyInterface(Type type) { if (!type.IsInterface || !type.IsConstructedGenericType) { return false; } @@ -815,7 +810,7 @@ private static bool IsArrayCompatibleReadOnlyInterface(Type type) || genericTypeDefinition == typeof(IReadOnlyList<>); } - private static bool IsSet(Type type) + private static bool TypeCanBeAssignedAHashSet(Type type) { if (!type.IsInterface || !type.IsConstructedGenericType) { return false; } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 7bc5c071ed2ee2..a37d570b667045 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -113,10 +113,28 @@ public class MyClassWithCustomCollection public interface ICustomSet : ISet { } + public interface ICustomCollection2 : ICollection + { + } + public class MyClassWithCustomSet { public ICustomSet CustomSet { get; set; } } + + public class MyClassWithCustomDictionary + { + public ICustomDictionary CustomDictionary { get; set; } + } + + public interface ICustomDictionary : IDictionary + { + } + + public class MyClassWithCustomCollection2 + { + public ICustomCollection2 CustomCollection { get; set; } + } public class NullableOptions @@ -721,6 +739,32 @@ public void SkipsCustomCollection() Assert.Null(instance.CustomCollection); } + [Fact] + public void SkipsCustomDictionary() + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection( new Dictionary + { + ["CustomCollection:0"] = "Yo!", + }); + var config = configurationBuilder.Build(); + var instance = config.Get()!; + Assert.Null(instance.CustomDictionary); + } + + [Fact] + public void SkipsCustomCollection2() + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection( new Dictionary + { + ["CustomCollection:0"] = "Yo!", + }); + var config = configurationBuilder.Build(); + var instance = config.Get()!; + Assert.Null(instance.CustomCollection); + } + [Fact] public void SkipsCustomSet() { From 92afd9aebd8bb86c20cc216f9cd862d71fcccd3e Mon Sep 17 00:00:00 2001 From: stevedunnhq Date: Sat, 23 Apr 2022 15:48:18 +0100 Subject: [PATCH 11/28] Tidy up and remove dead code. --- .../src/ConfigurationBinder.cs | 53 +++---------------- .../tests/ConfigurationBinderTests.cs | 2 + 2 files changed, 9 insertions(+), 46 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 0006bc7b80db5e..b7bb95cdbb6a6b 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -258,32 +258,13 @@ private static void BindProperty(PropertyInfo property, object instance, IConfig } } - [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the object collection in type so its members may be trimmed.")] - private static object BindToSet(Type type, IConfiguration config, BinderOptions options) - { - Type genericType = typeof(HashSet<>).MakeGenericType(type.GenericTypeArguments[0]); - object instance = Activator.CreateInstance(genericType)!; - BindCollection(instance, genericType, config, options); - return instance; - } - - [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the object collection in type so its members may be trimmed.")] - private static object BindToCollection(Type type, IConfiguration config, BinderOptions options) - { - Type genericType = typeof(List<>).MakeGenericType(type.GenericTypeArguments[0]); - object instance = Activator.CreateInstance(genericType)!; - BindCollection(instance, genericType, config, options); - return instance; - } - // Called when the binding point doesn't have a value. We need to determine the best type // to use given just an interface. - // If there is no best type to create, for instance, the user provided a custom interface that is `IEnumerable<>`, + // If there is no best type to create, for instance, the user provided a custom interface derived from `IEnumerable<>`, // then we return null. [RequiresUnreferencedCode("In case type is a Dictionary, cannot statically analyze what the element type is of the value objects in the dictionary so its members may be trimmed.")] private static (bool WasCollection, object? NewInstance) AttemptBindToCollectionInterfaces( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] - Type type, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type, IConfiguration config, BinderOptions options) { if (!type.IsInterface) @@ -300,22 +281,12 @@ private static (bool WasCollection, object? NewInstance) AttemptBindToCollection return (true, instance); } - if (TypeCanBeAssignedAHashSet(type)) - { - return (true, BindToSet(type, config, options)); - } - - if (TypeCanBeAssignedAList(type)) - { - return (true, BindToCollection(type, config, options)); - } - // The interface that we've been given is not something that can be assigned a hashset, list, or dictionary. // So all we need to return now is whether or not it was a IEnumerable. If it - // was a collection, the caller binds null to it, if it wasn't, the caller creates - // an instance of the type - - return (FindOpenGenericInterface(type, typeof(IEnumerable<>)) != null, null); + // *was* derivable from IEnumerable (i.e. we can't create it), then the caller binds + // null to it, and if it wasn't, then the caller creates an instance of the type. + bool wasACollection = FindOpenGenericInterface(type, typeof(IEnumerable<>)) != null; + return (wasACollection, null); } [RequiresUnreferencedCode(TrimmingWarningMessage)] @@ -780,17 +751,6 @@ private static bool TryConvertValue( return result; } - private static bool TypeCanBeAssignedAList(Type type) - { - if (!type.IsInterface || !type.IsConstructedGenericType) { return false; } - - Type genericTypeDefinition = type.GetGenericTypeDefinition(); - return genericTypeDefinition == typeof(IEnumerable<>) - || genericTypeDefinition == typeof(IReadOnlyCollection<>) - || genericTypeDefinition == typeof(ICollection<>) - || genericTypeDefinition == typeof(IList<>) - || genericTypeDefinition == typeof(IReadOnlyList<>); - } private static bool TypeCanBeAssignedADictionary(Type type) { if (!type.IsInterface || !type.IsConstructedGenericType) { return false; } @@ -806,6 +766,7 @@ private static bool IsArrayCompatibleReadOnlyInterface(Type type) Type genericTypeDefinition = type.GetGenericTypeDefinition(); return genericTypeDefinition == typeof(IEnumerable<>) + || genericTypeDefinition == typeof(ICollection<>) || genericTypeDefinition == typeof(IReadOnlyCollection<>) || genericTypeDefinition == typeof(IReadOnlyList<>); } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index a37d570b667045..d2dd431e5160a2 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -547,6 +547,7 @@ public void CanBindNonInstantiatedISet() { {"NonInstantiatedISet:0", "Yo1"}, {"NonInstantiatedISet:1", "Yo2"}, + {"NonInstantiatedISet:2", "Yo2"}, }; var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryCollection(dic); @@ -785,6 +786,7 @@ public void CanBindInstantiatedISet() { {"InstantiatedISet:0", "Yo1"}, {"InstantiatedISet:1", "Yo2"}, + {"InstantiatedISet:2", "Yo2"}, }; var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryCollection(dic); From 36878866963ca90f0a83fd301e3c14095a1197e2 Mon Sep 17 00:00:00 2001 From: stevedunnhq Date: Sat, 23 Apr 2022 15:55:07 +0100 Subject: [PATCH 12/28] Tidy up tests --- .../tests/ConfigurationBinderTests.cs | 42 ++++++++----------- 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index d2dd431e5160a2..4e196c9d262874 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -102,20 +102,20 @@ public override string Virtual } } - public interface ICustomCollection : IEnumerable - { - } - public class MyClassWithCustomCollection + public interface ICustomCollectionDerivedFromIEnumerableT : IEnumerable { } + public interface ICustomCollectionDerivedFromICollectionT : ICollection { } + + public class MyClassWithCustomCollections { - public ICustomCollection CustomCollection { get; set; } + public ICustomCollectionDerivedFromIEnumerableT CustomIEnumerableCollection { get; set; } + public ICustomCollectionDerivedFromICollectionT CustomCollection { get; set; } + + } public interface ICustomSet : ISet { } - public interface ICustomCollection2 : ICollection - { - } public class MyClassWithCustomSet { @@ -131,12 +131,6 @@ public interface ICustomDictionary : IDictionary { } - public class MyClassWithCustomCollection2 - { - public ICustomCollection2 CustomCollection { get; set; } - } - - public class NullableOptions { public bool? MyNullableBool { get; set; } @@ -728,20 +722,20 @@ public void CanBindInstantiatedDictionaryOfISetWithSomeExistingValues() } [Fact] - public void SkipsCustomCollection() + public void SkipsCustomIEnumerableCollection() { var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryCollection( new Dictionary { - ["CustomCollection:0"] = "Yo!", + ["CustomIEnumerableCollection:0"] = "Yo!", }); var config = configurationBuilder.Build(); - var instance = config.Get()!; - Assert.Null(instance.CustomCollection); + var instance = config.Get()!; + Assert.Null(instance.CustomIEnumerableCollection); } [Fact] - public void SkipsCustomDictionary() + public void SkipsCustomICollection() { var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryCollection( new Dictionary @@ -749,12 +743,12 @@ public void SkipsCustomDictionary() ["CustomCollection:0"] = "Yo!", }); var config = configurationBuilder.Build(); - var instance = config.Get()!; - Assert.Null(instance.CustomDictionary); + var instance = config.Get()!; + Assert.Null(instance.CustomCollection); } [Fact] - public void SkipsCustomCollection2() + public void SkipsCustomDictionary() { var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryCollection( new Dictionary @@ -762,8 +756,8 @@ public void SkipsCustomCollection2() ["CustomCollection:0"] = "Yo!", }); var config = configurationBuilder.Build(); - var instance = config.Get()!; - Assert.Null(instance.CustomCollection); + var instance = config.Get()!; + Assert.Null(instance.CustomDictionary); } [Fact] From 3c65aa48db1cd48778f099adbd89e9fc17c402e8 Mon Sep 17 00:00:00 2001 From: stevedunnhq Date: Sat, 23 Apr 2022 16:20:40 +0100 Subject: [PATCH 13/28] Tidy up comments --- .../src/ConfigurationBinder.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index b7bb95cdbb6a6b..b8bc89f71d6161 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -281,10 +281,9 @@ private static (bool WasCollection, object? NewInstance) AttemptBindToCollection return (true, instance); } - // The interface that we've been given is not something that can be assigned a hashset, list, or dictionary. - // So all we need to return now is whether or not it was a IEnumerable. If it - // *was* derivable from IEnumerable (i.e. we can't create it), then the caller binds - // null to it, and if it wasn't, then the caller creates an instance of the type. + // The interface that we've been given is not something that can be bound to as a collection. + // We return whether or not it was an IEnumerable<>. If it *was* (i.e. we can't create it), then the caller binds + // null to it, and if it wasn't (i.e. *not* a collection), then the caller creates an instance of it. bool wasACollection = FindOpenGenericInterface(type, typeof(IEnumerable<>)) != null; return (wasACollection, null); } From ee0b40bdb8caa082228a6d508dc87407e69bbeef Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Thu, 5 May 2022 22:03:59 +0100 Subject: [PATCH 14/28] Copy over @halter73's changes --- .../src/ConfigurationBinder.cs | 60 +++++++------------ 1 file changed, 21 insertions(+), 39 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index b8bc89f71d6161..4b2080ea1aa4b2 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -284,7 +284,7 @@ private static (bool WasCollection, object? NewInstance) AttemptBindToCollection // The interface that we've been given is not something that can be bound to as a collection. // We return whether or not it was an IEnumerable<>. If it *was* (i.e. we can't create it), then the caller binds // null to it, and if it wasn't (i.e. *not* a collection), then the caller creates an instance of it. - bool wasACollection = FindOpenGenericInterface(type, typeof(IEnumerable<>)) != null; + bool wasACollection = FindOpenGenericInterface(typeof(IEnumerable<>), type) != null; return (wasACollection, null); } @@ -362,7 +362,7 @@ private static void BindInstance( } // See if it's a Dictionary - Type? collectionInterface = FindOpenGenericInterface(type, typeof(IDictionary<,>)); + Type? collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type); if (collectionInterface != null) { BindDictionary(bindingPoint.Value!, collectionInterface, config, options); @@ -370,10 +370,10 @@ private static void BindInstance( else { // See if it's an ICollection - collectionInterface = FindOpenGenericInterface(type, typeof(ICollection<>)); + collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type); if (collectionInterface != null) { - BindCollection(bindingPoint.Value!, bindingPoint.Value!.GetType(), config, options); + BindCollection(bindingPoint.Value!, collectionInterface, config, options); } // Something else else @@ -551,18 +551,8 @@ private static void BindCollection( IConfiguration config, BinderOptions options) { // ICollection is guaranteed to have exactly one parameter - Type itemType = collectionType.GenericTypeArguments.Length == 0 ? typeof(object) : collectionType.GenericTypeArguments[0]; - - MethodInfo? addMethod = collectionType - .GetMethods(BindingFlags.Instance | BindingFlags.Public).SingleOrDefault(m => m.Name == "Add" && m.GetParameters().Length == 1); - - if (addMethod is null) - { - return; - } - - object?[] arguments = new object?[1]; - + Type itemType = collectionType.GenericTypeArguments[0]; + MethodInfo? addMethod = collectionType.GetMethod("Add", DeclaredOnlyLookup); foreach (IConfigurationSection section in config.GetChildren()) { try @@ -575,8 +565,7 @@ private static void BindCollection( options: options); if (itemBindingPoint.HasNewValue) { - arguments[0] = itemBindingPoint.Value; - addMethod?.Invoke(collection, arguments); + addMethod?.Invoke(collection, new[] { itemBindingPoint.Value }); } } catch @@ -782,34 +771,27 @@ private static bool TypeCanBeAssignedAHashSet(Type type) ; } - // Determines if the type is descended from the desired type. private static Type? FindOpenGenericInterface( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type type, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] Type desiredType) + Type expected, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces)] + Type actual) { - Type[] interfaceTypes = type.GetInterfaces(); - - foreach (Type it in interfaceTypes) + if (actual.IsGenericType && + actual.GetGenericTypeDefinition() == expected) { - if (it.IsGenericType && it.GetGenericTypeDefinition() == desiredType) - { - return type; - } + return actual; } - if (type.IsGenericType && type.GetGenericTypeDefinition() == desiredType) - { - return type; - } - - Type? baseType = type.BaseType; - - if (baseType == null) + Type[] interfaces = actual.GetInterfaces(); + foreach (Type interfaceType in interfaces) { - return null; + if (interfaceType.IsGenericType && + interfaceType.GetGenericTypeDefinition() == expected) + { + return interfaceType; + } } - - return FindOpenGenericInterface(baseType, desiredType); + return null; } Type[] interfaces = actual.GetInterfaces(); From 6b6f6cde69d59617d26f70fc0866de1e8126ca75 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Thu, 5 May 2022 22:12:52 +0100 Subject: [PATCH 15/28] Copy over @halter73's changes --- .../tests/ConfigurationBinderTests.cs | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 4e196c9d262874..b0e443263edbc2 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -60,16 +60,16 @@ public string ReadOnly { {"item1", new HashSet(new[] {"existing1", "existing2"})} }; - + public IEnumerable NonInstantiatedIEnumerable { get; set; } = null!; public ISet InstantiatedISet { get; set; } = new HashSet(); public ISet InstantiatedISetWithSomeValues { get; set; } = - new HashSet(new[] {"existing1", "existing2"}); + new HashSet(new[] { "existing1", "existing2" }); #if NET5_0_OR_GREATER public IReadOnlySet InstantiatedIReadOnlySet { get; set; } = new HashSet(); public IReadOnlySet InstantiatedIReadOnlySetWithSomeValues { get; set; } = - new HashSet(new[] {"existing1", "existing2"}); + new HashSet(new[] { "existing1", "existing2" }); public IReadOnlySet NonInstantiatedIReadOnlySet { get; set; } public IDictionary> InstantiatedDictionaryWithReadOnlySetWithSomeValues { get; set; } = new Dictionary> @@ -109,8 +109,6 @@ public class MyClassWithCustomCollections { public ICustomCollectionDerivedFromIEnumerableT CustomIEnumerableCollection { get; set; } public ICustomCollectionDerivedFromICollectionT CustomCollection { get; set; } - - } public interface ICustomSet : ISet @@ -722,55 +720,71 @@ public void CanBindInstantiatedDictionaryOfISetWithSomeExistingValues() } [Fact] - public void SkipsCustomIEnumerableCollection() + public void ThrowsForCustomIEnumerableCollection() { var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection( new Dictionary + configurationBuilder.AddInMemoryCollection(new Dictionary { ["CustomIEnumerableCollection:0"] = "Yo!", }); var config = configurationBuilder.Build(); - var instance = config.Get()!; - Assert.Null(instance.CustomIEnumerableCollection); + + var exception = Assert.Throws( + () => config.Get()); + Assert.Equal( + SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomCollectionDerivedFromIEnumerableT)), + exception.Message); } [Fact] - public void SkipsCustomICollection() + public void ThrowsForCustomICollection() { var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection( new Dictionary + configurationBuilder.AddInMemoryCollection(new Dictionary { ["CustomCollection:0"] = "Yo!", }); var config = configurationBuilder.Build(); - var instance = config.Get()!; - Assert.Null(instance.CustomCollection); + + var exception = Assert.Throws( + () => config.Get()); + Assert.Equal( + SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomCollectionDerivedFromICollectionT)), + exception.Message); } [Fact] - public void SkipsCustomDictionary() + public void ThrowsForCustomDictionary() { var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection( new Dictionary + configurationBuilder.AddInMemoryCollection(new Dictionary { - ["CustomCollection:0"] = "Yo!", + ["CustomDictionary:0"] = "Yo!", }); var config = configurationBuilder.Build(); - var instance = config.Get()!; - Assert.Null(instance.CustomDictionary); + + var exception = Assert.Throws( + () => config.Get()); + Assert.Equal( + SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomDictionary)), + exception.Message); } [Fact] - public void SkipsCustomSet() + public void ThrowsForCustomSet() { var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection( new Dictionary + configurationBuilder.AddInMemoryCollection(new Dictionary { ["CustomSet:0"] = "Yo!", }); var config = configurationBuilder.Build(); - var instance = config.Get()!; - Assert.Null(instance.CustomSet); + + var exception = Assert.Throws( + () => config.Get()); + Assert.Equal( + SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomSet)), + exception.Message); } [Fact] From 6efcd54107cf2b2aaddbfff889d6e1c2b9a36aa5 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Thu, 5 May 2022 22:23:05 +0100 Subject: [PATCH 16/28] Copy over @halter73's changes --- .../src/ConfigurationBinder.cs | 73 ++++++------------- 1 file changed, 23 insertions(+), 50 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 4b2080ea1aa4b2..155daa87d9ee22 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -202,7 +202,7 @@ public static void Bind(this IConfiguration configuration, object? instance, Act } [RequiresUnreferencedCode(PropertyTrimmingWarningMessage)] - private static void BindNonScalar(this IConfiguration configuration, object instance, BinderOptions options) + private static void BindProperties(object instance, IConfiguration configuration, BinderOptions options) { List modelProperties = GetAllProperties(instance.GetType()); @@ -258,36 +258,6 @@ private static void BindProperty(PropertyInfo property, object instance, IConfig } } - // Called when the binding point doesn't have a value. We need to determine the best type - // to use given just an interface. - // If there is no best type to create, for instance, the user provided a custom interface derived from `IEnumerable<>`, - // then we return null. - [RequiresUnreferencedCode("In case type is a Dictionary, cannot statically analyze what the element type is of the value objects in the dictionary so its members may be trimmed.")] - private static (bool WasCollection, object? NewInstance) AttemptBindToCollectionInterfaces( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type, - IConfiguration config, BinderOptions options) - { - if (!type.IsInterface) - { - return (false, null); - } - - if (TypeCanBeAssignedADictionary(type)) - { - Type typeOfKey = type.GenericTypeArguments[0]; - Type typeOfValue = type.GenericTypeArguments[1]; - object instance = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(typeOfKey, typeOfValue))!; - BindDictionary(instance, typeof(Dictionary<,>).MakeGenericType(typeOfKey, typeOfValue), config, options); - return (true, instance); - } - - // The interface that we've been given is not something that can be bound to as a collection. - // We return whether or not it was an IEnumerable<>. If it *was* (i.e. we can't create it), then the caller binds - // null to it, and if it wasn't (i.e. *not* a collection), then the caller creates an instance of it. - bool wasACollection = FindOpenGenericInterface(typeof(IEnumerable<>), type) != null; - return (wasACollection, null); - } - [RequiresUnreferencedCode(TrimmingWarningMessage)] private static void BindInstance( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type, @@ -329,7 +299,7 @@ private static void BindInstance( } // for sets and read-only set interfaces, we concatenate on to what is already there - if (TypeCanBeAssignedAHashSet(type)) + if (TypeIsASetInterface(type)) { if (!bindingPoint.IsReadOnly) { @@ -347,38 +317,40 @@ private static void BindInstance( return; } - (bool wasInterface, object? instance) = AttemptBindToCollectionInterfaces(type, config, options); - if (wasInterface) + // For other mutable interfaces like ICollection<> and ISet<>, we prefer copying values and setting them + // on a new instance of the interface over populating the existing instance implementing the interface. + // This has already been done, so there's not need to check again. For dictionaries, we fill the existing + // instance if there is one (which hasn't happened yet), and only create a new instance if necessary. + if (TypeIsADictionaryInterface(type)) { - if (instance != null) - { - bindingPoint.SetValue(instance); - } - - return; // We are already done if binding to a new collection instance worked + Type typeOfKey = type.GenericTypeArguments[0]; + Type typeOfValue = type.GenericTypeArguments[1]; + // Overwrite type in case it was a IReadOnlyDictionary<>. We still want to be able to bind items. + // REVIEW: What about settable IReadOnlyDictionary<> instances with an initial value? + // I think we should consider preferring copying like we do for all other collection interfaces. + type = typeof(Dictionary<,>).MakeGenericType(typeOfKey, typeOfValue); } bindingPoint.SetValue(CreateInstance(type, config, options)); } - // See if it's a Dictionary - Type? collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type); - if (collectionInterface != null) + // At this point we know that we have a non-null bindingPoint.Value, we just have to populate the items + // using the IDictionary<> or ICollection<> interfaces, or properties using reflection. + Type? dictionaryInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type); + if (dictionaryInterface != null) { - BindDictionary(bindingPoint.Value!, collectionInterface, config, options); + BindDictionary(bindingPoint.Value!, dictionaryInterface, config, options); } else { - // See if it's an ICollection - collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type); + Type? collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type); if (collectionInterface != null) { BindCollection(bindingPoint.Value!, collectionInterface, config, options); } - // Something else else { - BindNonScalar(config, bindingPoint.Value!, options); + BindProperties(bindingPoint.Value!, config, options); } } } @@ -553,6 +525,7 @@ private static void BindCollection( // ICollection is guaranteed to have exactly one parameter Type itemType = collectionType.GenericTypeArguments[0]; MethodInfo? addMethod = collectionType.GetMethod("Add", DeclaredOnlyLookup); + foreach (IConfigurationSection section in config.GetChildren()) { try @@ -739,7 +712,7 @@ private static bool TryConvertValue( return result; } - private static bool TypeCanBeAssignedADictionary(Type type) + private static bool TypeIsADictionaryInterface(Type type) { if (!type.IsInterface || !type.IsConstructedGenericType) { return false; } @@ -759,7 +732,7 @@ private static bool IsArrayCompatibleReadOnlyInterface(Type type) || genericTypeDefinition == typeof(IReadOnlyList<>); } - private static bool TypeCanBeAssignedAHashSet(Type type) + private static bool TypeIsASetInterface(Type type) { if (!type.IsInterface || !type.IsConstructedGenericType) { return false; } From 34549391960b388c919fbb7831efe21b7c4f5631 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Thu, 5 May 2022 22:37:38 +0100 Subject: [PATCH 17/28] Added restrictions and some tests for set keys being string/enum only --- .../src/ConfigurationBinder.cs | 20 +++++++-- .../tests/ConfigurationBinderTests.cs | 43 +++++++++++++++++++ 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 155daa87d9ee22..3b57aff94f8761 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -303,7 +303,11 @@ private static void BindInstance( { if (!bindingPoint.IsReadOnly) { - bindingPoint.SetValue(BindSet(type, (IEnumerable?)bindingPoint.Value, config, options)); + object? newValue = BindSet(type, (IEnumerable?)bindingPoint.Value, config, options); + if (newValue != null) + { + bindingPoint.SetValue(newValue); + } } return; } @@ -600,11 +604,21 @@ private static Array BindArray(Type type, IEnumerable? source, IConfiguration co } [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the Array so its members may be trimmed.")] - private static object BindSet(Type type, IEnumerable? source, IConfiguration config, BinderOptions options) + private static object? BindSet(Type type, IEnumerable? source, IConfiguration config, BinderOptions options) { Type elementType = type.GetGenericArguments()[0]; - Type genericType = typeof(HashSet<>).MakeGenericType(type.GenericTypeArguments[0]); + Type keyType = type.GenericTypeArguments[0]; + + bool keyTypeIsEnum = keyType.IsEnum; + + if (keyType != typeof(string) && !keyTypeIsEnum) + { + // We only support string and enum keys + return null; + } + + Type genericType = typeof(HashSet<>).MakeGenericType(keyType); object instance = Activator.CreateInstance(genericType)!; MethodInfo addMethod = genericType.GetMethod("Add", DeclaredOnlyLookup)!; diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index b0e443263edbc2..ba2f2e7069d909 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -66,6 +66,11 @@ public string ReadOnly public ISet InstantiatedISetWithSomeValues { get; set; } = new HashSet(new[] { "existing1", "existing2" }); + public ISet HashSetWithUnsupportedKey { get; set; } = + new HashSet(); + + public ISet UninstantiatedHashSetWithUnsupportedKey { get; set; } + #if NET5_0_OR_GREATER public IReadOnlySet InstantiatedIReadOnlySet { get; set; } = new HashSet(); public IReadOnlySet InstantiatedIReadOnlySetWithSomeValues { get; set; } = @@ -102,6 +107,8 @@ public override string Virtual } } + public class UnsupportedTypeInHashSet { } + public interface ICustomCollectionDerivedFromIEnumerableT : IEnumerable { } public interface ICustomCollectionDerivedFromICollectionT : ICollection { } @@ -830,6 +837,42 @@ public void CanBindInstantiatedISetWithSomeValues() Assert.Equal("Yo2", options.InstantiatedISetWithSomeValues.ElementAt(3)); } + [Fact] + public void DoesNotBindInstantiatedISetWithUnsupportedKeys() + { + var dic = new Dictionary + { + {"HashSetWithUnsupportedKey:0", "Yo1"}, + {"HashSetWithUnsupportedKey:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(0, options.HashSetWithUnsupportedKey.Count); + } + + [Fact] + public void DoesNotBindUninstantiatedISetWithUnsupportedKeys() + { + var dic = new Dictionary + { + {"UninstantiatedHashSetWithUnsupportedKey:0", "Yo1"}, + {"UninstantiatedHashSetWithUnsupportedKey:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Null(options.UninstantiatedHashSetWithUnsupportedKey); + } + [Fact] public void CanBindInstantiatedIEnumerableWithItems() { From 686192e3c318ab09a40a6df9dff56ab04126fc8b Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Mon, 30 May 2022 05:44:06 +0100 Subject: [PATCH 18/28] PR feedback re. preprocessor directive --- .../src/ConfigurationBinder.cs | 2 +- .../tests/ConfigurationBinderTests.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 3b57aff94f8761..91c6f7c0a62dd0 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -752,7 +752,7 @@ private static bool TypeIsASetInterface(Type type) Type genericTypeDefinition = type.GetGenericTypeDefinition(); return genericTypeDefinition == typeof(ISet<>) -#if NET5_0_OR_GREATER +#if NETCOREAPP || genericTypeDefinition == typeof(IReadOnlySet<>) #endif ; diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index ba2f2e7069d909..1d621e3ba243a3 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -71,7 +71,7 @@ public string ReadOnly public ISet UninstantiatedHashSetWithUnsupportedKey { get; set; } -#if NET5_0_OR_GREATER +#if NETCOREAPP public IReadOnlySet InstantiatedIReadOnlySet { get; set; } = new HashSet(); public IReadOnlySet InstantiatedIReadOnlySetWithSomeValues { get; set; } = new HashSet(new[] { "existing1", "existing2" }); @@ -560,7 +560,7 @@ public void CanBindNonInstantiatedISet() Assert.Equal("Yo2", options.NonInstantiatedISet.ElementAt(1)); } -#if NET5_0_OR_GREATER +#if NETCOREAPP [Fact] public void CanBindInstantiatedIReadOnlySet() { From b9a9f3ab5922362c881c0a29605f34864b85176c Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Mon, 30 May 2022 21:43:57 +0100 Subject: [PATCH 19/28] Started on instantiated dictionary feedback --- .../src/ConfigurationBinder.cs | 7 +- .../tests/ConfigurationBinderTests.cs | 153 ++++++++++++++++++ 2 files changed, 158 insertions(+), 2 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 91c6f7c0a62dd0..df9c2653da600e 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -341,9 +341,12 @@ private static void BindInstance( // At this point we know that we have a non-null bindingPoint.Value, we just have to populate the items // using the IDictionary<> or ICollection<> interfaces, or properties using reflection. Type? dictionaryInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type); - if (dictionaryInterface != null) + Type? dictionaryInterface2 = FindOpenGenericInterface(typeof(IReadOnlyDictionary<,>), type); + + Type? di = dictionaryInterface ?? dictionaryInterface2; + if (di != null) { - BindDictionary(bindingPoint.Value!, dictionaryInterface, config, options); + BindDictionary(bindingPoint.Value!, di, config, options); } else { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 1d621e3ba243a3..f6678f79be1771 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -15,6 +15,12 @@ public class ConfigurationBinderTests { public class ComplexOptions { + private static Dictionary _existingDictionary = new() + { + {"existing-item1", 1}, + {"existing-item2", 2}, + }; + public ComplexOptions() { Nested = new NestedOptions(); @@ -82,6 +88,14 @@ public string ReadOnly {"item1", new HashSet(new[] {"existing1", "existing2"})} }; #endif + public IReadOnlyDictionary InstantiatedReadOnlyDictionaryWithWithSomeValues { get; set; } = + _existingDictionary; + + public IReadOnlyDictionary NonInstantiatedReadOnlyDictionary { get; set; } + + public CustomICollectionWithoutAnAddMethod InstantiatedCustomICollectionWithoutAnAddMethod { get; set; } = new(); + public CustomICollectionWithoutAnAddMethod NonInstantiatedCustomICollectionWithoutAnAddMethod { get; set; } + public IEnumerable InstantiatedIEnumerable { get; set; } = new List(); public ICollection InstantiatedICollection { get; set; } = new List(); public IReadOnlyCollection InstantiatedIReadOnlyCollection { get; set; } = new List(); @@ -118,6 +132,27 @@ public class MyClassWithCustomCollections public ICustomCollectionDerivedFromICollectionT CustomCollection { get; set; } } + public class CustomICollectionWithoutAnAddMethod : ICollection + { + private readonly List _items = new(); + public IEnumerator GetEnumerator() => _items.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + void ICollection.Add(string item) => _items.Add(item); + + public void Clear() => _items.Clear(); + + public bool Contains(string item) => _items.Contains(item); + + public void CopyTo(string[] array, int arrayIndex) => _items.CopyTo(array, arrayIndex); + + public bool Remove(string item) => _items.Remove(item); + + public int Count => _items.Count; + public bool IsReadOnly => false; + } + public interface ICustomSet : ISet { } @@ -651,6 +686,84 @@ public void CanBindInstantiatedDictionaryOfIReadOnlySetWithSomeExistingValues() } #endif + public class Foo + { + public IReadOnlyDictionary Items { get; set; } = + new Dictionary {{"existing-item1", 1}, {"existing-item2", 2}}; + + } + + [Fact] + public void CanBindInstantiatedReadOnlyDictionary2() + { + var dic = new Dictionary + { + {"Items:item3", "3"}, + {"Items:item4", "4"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(4, options.Items.Count); + Assert.Equal(1, options.Items["existing-item1"]); + Assert.Equal(2, options.Items["existing-item2"]); + Assert.Equal(3, options.Items["item3"]); + Assert.Equal(4, options.Items["item4"]); + + + } + + [Fact] + public void CanBindInstantiatedReadOnlyDictionary() + { + var dic = new Dictionary + { + {"InstantiatedReadOnlyDictionaryWithWithSomeValues:item3", "3"}, + {"InstantiatedReadOnlyDictionaryWithWithSomeValues:item4", "4"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(4, options.InstantiatedReadOnlyDictionaryWithWithSomeValues.Count); + Assert.Equal(1, options.InstantiatedReadOnlyDictionaryWithWithSomeValues["existing-item1"]); + Assert.Equal(2, options.InstantiatedReadOnlyDictionaryWithWithSomeValues["existing-item2"]); + Assert.Equal(3, options.InstantiatedReadOnlyDictionaryWithWithSomeValues["item3"]); + Assert.Equal(4, options.InstantiatedReadOnlyDictionaryWithWithSomeValues["item4"]); + + + } + + [Fact] + public void CanBindNonInstantiatedReadOnlyDictionary() + { + var dic = new Dictionary + { + {"NonInstantiatedReadOnlyDictionary:item3", "3"}, + {"NonInstantiatedReadOnlyDictionary:item4", "4"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.NonInstantiatedReadOnlyDictionary.Count); + Assert.Equal(3, options.NonInstantiatedReadOnlyDictionary["item3"]); + Assert.Equal(4, options.NonInstantiatedReadOnlyDictionary["item4"]); + + + } + + [Fact] public void CanBindNonInstantiatedDictionaryOfISet() { @@ -893,6 +1006,46 @@ public void CanBindInstantiatedIEnumerableWithItems() Assert.Equal("Yo2", options.InstantiatedIEnumerable.ElementAt(1)); } + [Fact] + public void CanBindInstantiatedCustomICollectionWithoutAnAddMethodWithItems() + { + var dic = new Dictionary + { + {"InstantiatedCustomICollectionWithoutAnAddMethod:0", "Yo1"}, + {"InstantiatedCustomICollectionWithoutAnAddMethod:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.InstantiatedCustomICollectionWithoutAnAddMethod.Count); + Assert.Equal("Yo1", options.InstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(0)); + Assert.Equal("Yo2", options.InstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(1)); + } + + [Fact] + public void CanBindNonInstantiatedCustomICollectionWithoutAnAddMethodWithItems() + { + var dic = new Dictionary + { + {"NonInstantiatedCustomICollectionWithoutAnAddMethod:0", "Yo1"}, + {"NonInstantiatedCustomICollectionWithoutAnAddMethod:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.NonInstantiatedCustomICollectionWithoutAnAddMethod.Count); + Assert.Equal("Yo1", options.NonInstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(0)); + Assert.Equal("Yo2", options.NonInstantiatedCustomICollectionWithoutAnAddMethod.ElementAt(1)); + } + [Fact] public void CanBindInstantiatedICollectionWithItems() { From 32609c8403822251015d6f01cfb7023d850dc972 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Sat, 4 Jun 2022 06:18:56 +0100 Subject: [PATCH 20/28] First pass at cloning dictionary<,> --- .../src/ConfigurationBinder.cs | 24 ++++++++++++ .../tests/ConfigurationBinderTests.cs | 37 +++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index df9c2653da600e..e2cac7706ad9b1 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -492,6 +492,30 @@ private static void BindDictionary( // We only support string and enum keys return; } + + Type genericType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); + Type kvpType = typeof(KeyValuePair<,>).MakeGenericType(keyType, valueType); + MethodInfo addMethod = genericType.GetMethod("Add", DeclaredOnlyLookup)!; + PropertyInfo keyMethod = kvpType.GetProperty("Key", DeclaredOnlyLookup)!; + PropertyInfo valueMethod = kvpType.GetProperty("Value", DeclaredOnlyLookup)!; + + object instance = Activator.CreateInstance(genericType)!; + + var source = dictionary as IEnumerable; + object?[] arguments = new object?[2]; + + if (source != null) + { + foreach (object? item in source) + { + object? k = keyMethod.GetMethod!.Invoke(item, null); + object? v = valueMethod.GetMethod!.Invoke(item, null); + arguments[0] = k; + arguments[1] = v; + addMethod.Invoke(instance, arguments); + } + } + MethodInfo tryGetValue = dictionaryType.GetMethod("TryGetValue")!; PropertyInfo setter = dictionaryType.GetProperty("Item", DeclaredOnlyLookup)!; foreach (IConfigurationSection child in config.GetChildren()) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index f6678f79be1771..0f9926218304c2 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -13,6 +13,19 @@ namespace Microsoft.Extensions.Configuration.Binder.Test { public class ConfigurationBinderTests { + public class Steve + { + private static Dictionary _existingDictionary = new() + { + {"existing-item1", 1}, + {"existing-item2", 2}, + }; + + public IReadOnlyDictionary Dictionary { get; set; } = + _existingDictionary; + + } + public class ComplexOptions { private static Dictionary _existingDictionary = new() @@ -715,6 +728,30 @@ public void CanBindInstantiatedReadOnlyDictionary2() Assert.Equal(4, options.Items["item4"]); + } + + [Fact] + public void SteveCanBindInstantiatedReadOnlyDictionary() + { + var dic = new Dictionary + { + {"Dictionary:item3", "3"}, + {"Dictionary:item4", "4"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(4, options.Dictionary.Count); + Assert.Equal(1, options.Dictionary["existing-item1"]); + Assert.Equal(2, options.Dictionary["existing-item2"]); + Assert.Equal(3, options.Dictionary["item3"]); + Assert.Equal(4, options.Dictionary["item4"]); + + } [Fact] From 1310ebd1f3f98807042cb883fde5f8d843162991 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Sat, 4 Jun 2022 07:03:31 +0100 Subject: [PATCH 21/28] Can now bind (overwrite) to an existing concrete dictionary --- .../src/ConfigurationBinder.cs | 88 ++++++++++++++++--- .../tests/ConfigurationBinderTests.cs | 6 +- 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index e2cac7706ad9b1..59f5a4db3692b1 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -312,6 +312,23 @@ private static void BindInstance( return; } + // For other mutable interfaces like ICollection<> and ISet<>, we prefer copying values and setting them + // on a new instance of the interface over populating the existing instance implementing the interface. + // This has already been done, so there's not need to check again. For dictionaries, we fill the existing + // instance if there is one (which hasn't happened yet), and only create a new instance if necessary. + if (TypeIsADictionaryInterface(type)) + { + if (!bindingPoint.IsReadOnly) + { + object? newValue = BindDictionary(bindingPoint.Value, type, config, options); + if (newValue != null) + { + bindingPoint.SetValue(newValue); + } + } + return; + } + // If we don't have an instance, try to create one if (bindingPoint.Value is null) { @@ -346,7 +363,7 @@ private static void BindInstance( Type? di = dictionaryInterface ?? dictionaryInterface2; if (di != null) { - BindDictionary(bindingPoint.Value!, di, config, options); + BindExistingDictionary(bindingPoint.Value!, di, config, options); } else { @@ -476,8 +493,8 @@ private static bool CanBindToTheseConstructorParameters(ParameterInfo[] construc } [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the value objects in the dictionary so its members may be trimmed.")] - private static void BindDictionary( - object dictionary, + private static object? BindDictionary( + object? source, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type dictionaryType, IConfiguration config, BinderOptions options) @@ -490,7 +507,7 @@ private static void BindDictionary( if (keyType != typeof(string) && !keyTypeIsEnum) { // We only support string and enum keys - return; + return null; } Type genericType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); @@ -499,25 +516,25 @@ private static void BindDictionary( PropertyInfo keyMethod = kvpType.GetProperty("Key", DeclaredOnlyLookup)!; PropertyInfo valueMethod = kvpType.GetProperty("Value", DeclaredOnlyLookup)!; - object instance = Activator.CreateInstance(genericType)!; + object dictionary = Activator.CreateInstance(genericType)!; - var source = dictionary as IEnumerable; + var orig = source as IEnumerable; object?[] arguments = new object?[2]; - if (source != null) + if (orig != null) { - foreach (object? item in source) + foreach (object? item in orig) { object? k = keyMethod.GetMethod!.Invoke(item, null); object? v = valueMethod.GetMethod!.Invoke(item, null); arguments[0] = k; arguments[1] = v; - addMethod.Invoke(instance, arguments); + addMethod.Invoke(dictionary, arguments); } } MethodInfo tryGetValue = dictionaryType.GetMethod("TryGetValue")!; - PropertyInfo setter = dictionaryType.GetProperty("Item", DeclaredOnlyLookup)!; + PropertyInfo setter = genericType.GetProperty("Item", DeclaredOnlyLookup)!; foreach (IConfigurationSection child in config.GetChildren()) { try @@ -544,6 +561,57 @@ private static void BindDictionary( { } } + + return dictionary; + } + + [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the value objects in the dictionary so its members may be trimmed.")] + private static void BindExistingDictionary( + object? dictionary, + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] + Type dictionaryType, + IConfiguration config, BinderOptions options) + { + Type keyType = dictionaryType.GenericTypeArguments[0]; + Type valueType = dictionaryType.GenericTypeArguments[1]; + bool keyTypeIsEnum = keyType.IsEnum; + + if (keyType != typeof(string) && !keyTypeIsEnum) + { + // We only support string and enum keys + return; + } + + Type genericType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); + + MethodInfo tryGetValue = dictionaryType.GetMethod("TryGetValue")!; + PropertyInfo setter = genericType.GetProperty("Item", DeclaredOnlyLookup)!; + foreach (IConfigurationSection child in config.GetChildren()) + { + try + { + object key = keyTypeIsEnum ? Enum.Parse(keyType, child.Key) : child.Key; + var valueBindingPoint = new BindingPoint( + initialValueProvider: () => + { + object?[] tryGetValueArgs = { key, null }; + return (bool)tryGetValue.Invoke(dictionary, tryGetValueArgs)! ? tryGetValueArgs[1] : null; + }, + isReadOnly: false); + BindInstance( + type: valueType, + bindingPoint: valueBindingPoint, + config: child, + options: options); + if (valueBindingPoint.HasNewValue) + { + setter.SetValue(dictionary, valueBindingPoint.Value, new object[] { key }); + } + } + catch + { + } + } } [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the object collection so its members may be trimmed.")] diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 0f9926218304c2..f6a855775f627c 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -15,7 +15,7 @@ public class ConfigurationBinderTests { public class Steve { - private static Dictionary _existingDictionary = new() + public static Dictionary _existingDictionary = new() { {"existing-item1", 1}, {"existing-item2", 2}, @@ -735,6 +735,7 @@ public void SteveCanBindInstantiatedReadOnlyDictionary() { var dic = new Dictionary { + {"Dictionary:existing-item1", "666"}, {"Dictionary:item3", "3"}, {"Dictionary:item4", "4"} }; @@ -746,7 +747,8 @@ public void SteveCanBindInstantiatedReadOnlyDictionary() var options = config.Get()!; Assert.Equal(4, options.Dictionary.Count); - Assert.Equal(1, options.Dictionary["existing-item1"]); + Assert.Equal(1, Steve._existingDictionary["existing-item1"]); + Assert.Equal(666, options.Dictionary["existing-item1"]); Assert.Equal(2, options.Dictionary["existing-item2"]); Assert.Equal(3, options.Dictionary["item3"]); Assert.Equal(4, options.Dictionary["item4"]); From da49d5b98e349097a57de86d2a1f2f4f66e10645 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Sat, 4 Jun 2022 07:26:07 +0100 Subject: [PATCH 22/28] Refactor out common code for binding to existing dictionary. Added tests. --- .../src/ConfigurationBinder.cs | 46 ++++------- .../tests/ConfigurationBinderTests.cs | 76 +++++++++++++++---- 2 files changed, 73 insertions(+), 49 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 59f5a4db3692b1..ecf41a222e8d13 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -320,7 +320,7 @@ private static void BindInstance( { if (!bindingPoint.IsReadOnly) { - object? newValue = BindDictionary(bindingPoint.Value, type, config, options); + object? newValue = BindDictionaryInterface(bindingPoint.Value, type, config, options); if (newValue != null) { bindingPoint.SetValue(newValue); @@ -363,7 +363,7 @@ private static void BindInstance( Type? di = dictionaryInterface ?? dictionaryInterface2; if (di != null) { - BindExistingDictionary(bindingPoint.Value!, di, config, options); + BindConcreteDictionary(bindingPoint.Value!, di, config, options); } else { @@ -493,7 +493,7 @@ private static bool CanBindToTheseConstructorParameters(ParameterInfo[] construc } [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the value objects in the dictionary so its members may be trimmed.")] - private static object? BindDictionary( + private static object? BindDictionaryInterface( object? source, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type dictionaryType, @@ -511,8 +511,9 @@ private static bool CanBindToTheseConstructorParameters(ParameterInfo[] construc } Type genericType = typeof(Dictionary<,>).MakeGenericType(keyType, valueType); - Type kvpType = typeof(KeyValuePair<,>).MakeGenericType(keyType, valueType); MethodInfo addMethod = genericType.GetMethod("Add", DeclaredOnlyLookup)!; + + Type kvpType = typeof(KeyValuePair<,>).MakeGenericType(keyType, valueType); PropertyInfo keyMethod = kvpType.GetProperty("Key", DeclaredOnlyLookup)!; PropertyInfo valueMethod = kvpType.GetProperty("Value", DeclaredOnlyLookup)!; @@ -533,40 +534,19 @@ private static bool CanBindToTheseConstructorParameters(ParameterInfo[] construc } } - MethodInfo tryGetValue = dictionaryType.GetMethod("TryGetValue")!; - PropertyInfo setter = genericType.GetProperty("Item", DeclaredOnlyLookup)!; - foreach (IConfigurationSection child in config.GetChildren()) - { - try - { - object key = keyTypeIsEnum ? Enum.Parse(keyType, child.Key) : child.Key; - var valueBindingPoint = new BindingPoint( - initialValueProvider: () => - { - var tryGetValueArgs = new object?[] { key, null }; - return (bool)tryGetValue.Invoke(dictionary, tryGetValueArgs)! ? tryGetValueArgs[1] : null; - }, - isReadOnly: false); - BindInstance( - type: valueType, - bindingPoint: valueBindingPoint, - config: child, - options: options); - if (valueBindingPoint.HasNewValue) - { - setter.SetValue(dictionary, valueBindingPoint.Value, new object[] { key }); - } - } - catch - { - } - } + BindConcreteDictionary(dictionary, dictionaryType, config, options); return dictionary; } + // Binds and potentially overwrites a concrete dictionary. + // This differs from BindDictionaryInterface because this method doesn't clone + // the dictionary; it sets and/or overwrites values directly. + // When a user specifies a concrete dictionary in their config class, then that + // value is used as-us. When a user specifies an interface (instantiated) in their config class, + // then it is cloned to a new dictionary, the same way as other collections. [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the value objects in the dictionary so its members may be trimmed.")] - private static void BindExistingDictionary( + private static void BindConcreteDictionary( object? dictionary, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type dictionaryType, diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index f6a855775f627c..173280a5e2662c 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -13,19 +13,6 @@ namespace Microsoft.Extensions.Configuration.Binder.Test { public class ConfigurationBinderTests { - public class Steve - { - public static Dictionary _existingDictionary = new() - { - {"existing-item1", 1}, - {"existing-item2", 2}, - }; - - public IReadOnlyDictionary Dictionary { get; set; } = - _existingDictionary; - - } - public class ComplexOptions { private static Dictionary _existingDictionary = new() @@ -180,6 +167,32 @@ public class MyClassWithCustomDictionary public ICustomDictionary CustomDictionary { get; set; } } + public class ConfigWithInstantiatedIReadOnlyDictionary + { + public static Dictionary _existingDictionary = new() + { + {"existing-item1", 1}, + {"existing-item2", 2}, + }; + + public IReadOnlyDictionary Dictionary { get; set; } = + _existingDictionary; + + } + public class ConfigWithInstantiatedConcreteDictionary + { + public static Dictionary _existingDictionary = new() + { + {"existing-item1", 1}, + {"existing-item2", 2}, + }; + + public Dictionary Dictionary { get; set; } = + _existingDictionary; + + } + + public interface ICustomDictionary : IDictionary { } @@ -731,7 +744,36 @@ public void CanBindInstantiatedReadOnlyDictionary2() } [Fact] - public void SteveCanBindInstantiatedReadOnlyDictionary() + public void BindInstantiatedIReadOnlyDictionary_CreatesCopyOfOriginal() + { + var dic = new Dictionary + { + {"Dictionary:existing-item1", "666"}, + {"Dictionary:item3", "3"}, + {"Dictionary:item4", "4"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(4, options.Dictionary.Count); + + // does not overwrite original + Assert.Equal(1, ConfigWithInstantiatedIReadOnlyDictionary._existingDictionary["existing-item1"]); + + Assert.Equal(666, options.Dictionary["existing-item1"]); + Assert.Equal(2, options.Dictionary["existing-item2"]); + Assert.Equal(3, options.Dictionary["item3"]); + Assert.Equal(4, options.Dictionary["item4"]); + + + } + + [Fact] + public void BindInstantiatedConcreteDictionary_OverwritesOriginal() { var dic = new Dictionary { @@ -744,10 +786,12 @@ public void SteveCanBindInstantiatedReadOnlyDictionary() var config = configurationBuilder.Build(); - var options = config.Get()!; + var options = config.Get()!; Assert.Equal(4, options.Dictionary.Count); - Assert.Equal(1, Steve._existingDictionary["existing-item1"]); + + // overwrites original + Assert.Equal(666, ConfigWithInstantiatedConcreteDictionary._existingDictionary["existing-item1"]); Assert.Equal(666, options.Dictionary["existing-item1"]); Assert.Equal(2, options.Dictionary["existing-item2"]); Assert.Equal(3, options.Dictionary["item3"]); From 3f359f48e8518ef8dd93ba7b062358ffff9fd77c Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Sat, 4 Jun 2022 07:35:37 +0100 Subject: [PATCH 23/28] Comments and more tests --- .../tests/ConfigurationBinderTests.cs | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 173280a5e2662c..42a77d25a4b86b 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -191,7 +191,6 @@ public class ConfigWithInstantiatedConcreteDictionary _existingDictionary; } - public interface ICustomDictionary : IDictionary { @@ -749,8 +748,7 @@ public void BindInstantiatedIReadOnlyDictionary_CreatesCopyOfOriginal() var dic = new Dictionary { {"Dictionary:existing-item1", "666"}, - {"Dictionary:item3", "3"}, - {"Dictionary:item4", "4"} + {"Dictionary:item3", "3"} }; var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryCollection(dic); @@ -759,7 +757,7 @@ public void BindInstantiatedIReadOnlyDictionary_CreatesCopyOfOriginal() var options = config.Get()!; - Assert.Equal(4, options.Dictionary.Count); + Assert.Equal(3, options.Dictionary.Count); // does not overwrite original Assert.Equal(1, ConfigWithInstantiatedIReadOnlyDictionary._existingDictionary["existing-item1"]); @@ -767,9 +765,6 @@ public void BindInstantiatedIReadOnlyDictionary_CreatesCopyOfOriginal() Assert.Equal(666, options.Dictionary["existing-item1"]); Assert.Equal(2, options.Dictionary["existing-item2"]); Assert.Equal(3, options.Dictionary["item3"]); - Assert.Equal(4, options.Dictionary["item4"]); - - } [Fact] @@ -778,8 +773,7 @@ public void BindInstantiatedConcreteDictionary_OverwritesOriginal() var dic = new Dictionary { {"Dictionary:existing-item1", "666"}, - {"Dictionary:item3", "3"}, - {"Dictionary:item4", "4"} + {"Dictionary:item3", "3"} }; var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryCollection(dic); @@ -788,16 +782,13 @@ public void BindInstantiatedConcreteDictionary_OverwritesOriginal() var options = config.Get()!; - Assert.Equal(4, options.Dictionary.Count); + Assert.Equal(3, options.Dictionary.Count); // overwrites original Assert.Equal(666, ConfigWithInstantiatedConcreteDictionary._existingDictionary["existing-item1"]); Assert.Equal(666, options.Dictionary["existing-item1"]); Assert.Equal(2, options.Dictionary["existing-item2"]); Assert.Equal(3, options.Dictionary["item3"]); - Assert.Equal(4, options.Dictionary["item4"]); - - } [Fact] @@ -815,11 +806,12 @@ public void CanBindInstantiatedReadOnlyDictionary() var options = config.Get()!; - Assert.Equal(4, options.InstantiatedReadOnlyDictionaryWithWithSomeValues.Count); - Assert.Equal(1, options.InstantiatedReadOnlyDictionaryWithWithSomeValues["existing-item1"]); - Assert.Equal(2, options.InstantiatedReadOnlyDictionaryWithWithSomeValues["existing-item2"]); - Assert.Equal(3, options.InstantiatedReadOnlyDictionaryWithWithSomeValues["item3"]); - Assert.Equal(4, options.InstantiatedReadOnlyDictionaryWithWithSomeValues["item4"]); + var resultingDictionary = options.InstantiatedReadOnlyDictionaryWithWithSomeValues; + Assert.Equal(4, resultingDictionary.Count); + Assert.Equal(1, resultingDictionary["existing-item1"]); + Assert.Equal(2, resultingDictionary["existing-item2"]); + Assert.Equal(3, resultingDictionary["item3"]); + Assert.Equal(4, resultingDictionary["item4"]); } From c015c01db5c185dd66911da5c62fbf28018f95ce Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Sat, 4 Jun 2022 07:48:48 +0100 Subject: [PATCH 24/28] Add explicit (separate) test for binding to non-instantiated IReadOnlyDictionary --- .../tests/ConfigurationBinderTests.cs | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 42a77d25a4b86b..79ffa9e0d6fab3 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -179,6 +179,12 @@ public class ConfigWithInstantiatedIReadOnlyDictionary _existingDictionary; } + + public class ConfigWithNonInstantiatedReadOnlyDictionary + { + public IReadOnlyDictionary Dictionary { get; set; } = null!; + } + public class ConfigWithInstantiatedConcreteDictionary { public static Dictionary _existingDictionary = new() @@ -767,6 +773,27 @@ public void BindInstantiatedIReadOnlyDictionary_CreatesCopyOfOriginal() Assert.Equal(3, options.Dictionary["item3"]); } + [Fact] + public void BindNonInstantiatedIReadOnlyDictionary() + { + var dic = new Dictionary + { + {"Dictionary:item1", "1"}, + {"Dictionary:item2", "2"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.Dictionary.Count); + + Assert.Equal(1, options.Dictionary["item1"]); + Assert.Equal(2, options.Dictionary["item2"]); + } + [Fact] public void BindInstantiatedConcreteDictionary_OverwritesOriginal() { @@ -812,8 +839,6 @@ public void CanBindInstantiatedReadOnlyDictionary() Assert.Equal(2, resultingDictionary["existing-item2"]); Assert.Equal(3, resultingDictionary["item3"]); Assert.Equal(4, resultingDictionary["item4"]); - - } [Fact] @@ -834,8 +859,6 @@ public void CanBindNonInstantiatedReadOnlyDictionary() Assert.Equal(2, options.NonInstantiatedReadOnlyDictionary.Count); Assert.Equal(3, options.NonInstantiatedReadOnlyDictionary["item3"]); Assert.Equal(4, options.NonInstantiatedReadOnlyDictionary["item4"]); - - } From 93cf8c0d9e47d2712a78b16f60a95dec1279a84c Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Sun, 12 Jun 2022 17:54:51 +0100 Subject: [PATCH 25/28] PR feedback - add tests --- .../tests/ConfigurationBinderTests.cs | 99 ++++++++++++++++++- 1 file changed, 96 insertions(+), 3 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 79ffa9e0d6fab3..0731ff20419873 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -57,6 +57,7 @@ public string ReadOnly } public ISet NonInstantiatedISet { get; set; } = null!; + public HashSet NonInstantiatedHashSet { get; set; } = null!; public IDictionary> NonInstantiatedDictionaryWithISet { get; set; } = null!; public IDictionary> InstantiatedDictionaryWithHashSet { get; set; } = new Dictionary>(); @@ -68,10 +69,20 @@ public string ReadOnly }; public IEnumerable NonInstantiatedIEnumerable { get; set; } = null!; + public ISet InstantiatedISet { get; set; } = new HashSet(); + + public HashSet InstantiatedHashSetWithSomeValues { get; set; } = + new HashSet(new[] {"existing1", "existing2"}); + + public SortedSet InstantiatedSortedSetWithSomeValues { get; set; } = + new SortedSet(new[] {"existing1", "existing2"}); + + public SortedSet NonInstantiatedSortedSetWithSomeValues { get; set; } = null!; + public ISet InstantiatedISetWithSomeValues { get; set; } = new HashSet(new[] { "existing1", "existing2" }); - + public ISet HashSetWithUnsupportedKey { get; set; } = new HashSet(); @@ -177,7 +188,6 @@ public class ConfigWithInstantiatedIReadOnlyDictionary public IReadOnlyDictionary Dictionary { get; set; } = _existingDictionary; - } public class ConfigWithNonInstantiatedReadOnlyDictionary @@ -195,7 +205,6 @@ public class ConfigWithInstantiatedConcreteDictionary public Dictionary Dictionary { get; set; } = _existingDictionary; - } public interface ICustomDictionary : IDictionary @@ -1048,6 +1057,90 @@ public void CanBindInstantiatedISetWithSomeValues() Assert.Equal("Yo2", options.InstantiatedISetWithSomeValues.ElementAt(3)); } + [Fact] + public void CanBindInstantiatedHashSetWithSomeValues() + { + var dic = new Dictionary + { + {"InstantiatedHashSetWithSomeValues:0", "Yo1"}, + {"InstantiatedHashSetWithSomeValues:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(4, options.InstantiatedHashSetWithSomeValues.Count); + Assert.Equal("existing1", options.InstantiatedHashSetWithSomeValues.ElementAt(0)); + Assert.Equal("existing2", options.InstantiatedHashSetWithSomeValues.ElementAt(1)); + Assert.Equal("Yo1", options.InstantiatedHashSetWithSomeValues.ElementAt(2)); + Assert.Equal("Yo2", options.InstantiatedHashSetWithSomeValues.ElementAt(3)); + } + + [Fact] + public void CanBindNonInstantiatedHashSet() + { + var dic = new Dictionary + { + {"NonInstantiatedHashSet:0", "Yo1"}, + {"NonInstantiatedHashSet:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.NonInstantiatedHashSet.Count); + Assert.Equal("Yo1", options.NonInstantiatedHashSet.ElementAt(0)); + Assert.Equal("Yo2", options.NonInstantiatedHashSet.ElementAt(1)); + } + + [Fact] + public void CanBindInstantiatedSortedSetWithSomeValues() + { + var dic = new Dictionary + { + {"InstantiatedSortedSetWithSomeValues:0", "Yo1"}, + {"InstantiatedSortedSetWithSomeValues:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(4, options.InstantiatedSortedSetWithSomeValues.Count); + Assert.Equal("existing1", options.InstantiatedSortedSetWithSomeValues.ElementAt(0)); + Assert.Equal("existing2", options.InstantiatedSortedSetWithSomeValues.ElementAt(1)); + Assert.Equal("Yo1", options.InstantiatedSortedSetWithSomeValues.ElementAt(2)); + Assert.Equal("Yo2", options.InstantiatedSortedSetWithSomeValues.ElementAt(3)); + } + + [Fact] + public void CanBindNonInstantiatedSortedSetWithSomeValues() + { + var dic = new Dictionary + { + {"NonInstantiatedSortedSetWithSomeValues:0", "Yo1"}, + {"NonInstantiatedSortedSetWithSomeValues:1", "Yo2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(2, options.NonInstantiatedSortedSetWithSomeValues.Count); + Assert.Equal("Yo1", options.NonInstantiatedSortedSetWithSomeValues.ElementAt(0)); + Assert.Equal("Yo2", options.NonInstantiatedSortedSetWithSomeValues.ElementAt(1)); + } + [Fact] public void DoesNotBindInstantiatedISetWithUnsupportedKeys() { From 1667f86e6f2e9ed13cca33e73bfac5f99ee5c5b8 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Mon, 13 Jun 2022 21:27:37 +0100 Subject: [PATCH 26/28] PR feedback: introdcue variable for `typeIsADictionaryInterface' --- .../src/ConfigurationBinder.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index ecf41a222e8d13..1e7ea85cb3d582 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -312,11 +312,13 @@ private static void BindInstance( return; } + bool typeIsADictionaryInterface = TypeIsADictionaryInterface(type); + // For other mutable interfaces like ICollection<> and ISet<>, we prefer copying values and setting them // on a new instance of the interface over populating the existing instance implementing the interface. // This has already been done, so there's not need to check again. For dictionaries, we fill the existing // instance if there is one (which hasn't happened yet), and only create a new instance if necessary. - if (TypeIsADictionaryInterface(type)) + if (typeIsADictionaryInterface) { if (!bindingPoint.IsReadOnly) { @@ -342,7 +344,7 @@ private static void BindInstance( // on a new instance of the interface over populating the existing instance implementing the interface. // This has already been done, so there's not need to check again. For dictionaries, we fill the existing // instance if there is one (which hasn't happened yet), and only create a new instance if necessary. - if (TypeIsADictionaryInterface(type)) + if (typeIsADictionaryInterface) { Type typeOfKey = type.GenericTypeArguments[0]; Type typeOfValue = type.GenericTypeArguments[1]; From ca1defe3dc72677ccacb61d5d61e2883708118f8 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Mon, 13 Jun 2022 22:31:14 +0100 Subject: [PATCH 27/28] PR feedback --- .../src/ConfigurationBinder.cs | 32 ++++--------- .../ConfigurationCollectionBindingTests.cs | 45 ++++++++++++++++++- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 1e7ea85cb3d582..3e0cc77fd176ab 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -288,8 +288,8 @@ private static void BindInstance( if (config != null && config.GetChildren().Any()) { - // for arrays and read-only list-like interfaces, we concatenate on to what is already there - if (type.IsArray || IsArrayCompatibleReadOnlyInterface(type)) + // for arrays, collections, and read-only list-like interfaces, we concatenate on to what is already there + if (type.IsArray || IsArrayCompatibleInterface(type)) { if (!bindingPoint.IsReadOnly) { @@ -298,7 +298,7 @@ private static void BindInstance( return; } - // for sets and read-only set interfaces, we concatenate on to what is already there + // for sets and read-only set interfaces, we clone what's there into a new collection. if (TypeIsASetInterface(type)) { if (!bindingPoint.IsReadOnly) @@ -312,13 +312,10 @@ private static void BindInstance( return; } - bool typeIsADictionaryInterface = TypeIsADictionaryInterface(type); - - // For other mutable interfaces like ICollection<> and ISet<>, we prefer copying values and setting them + // For other mutable interfaces like ICollection<>, IDictionary<,> and ISet<>, we prefer copying values and setting them // on a new instance of the interface over populating the existing instance implementing the interface. - // This has already been done, so there's not need to check again. For dictionaries, we fill the existing - // instance if there is one (which hasn't happened yet), and only create a new instance if necessary. - if (typeIsADictionaryInterface) + // This has already been done, so there's not need to check again. + if (TypeIsADictionaryInterface(type)) { if (!bindingPoint.IsReadOnly) { @@ -344,15 +341,6 @@ private static void BindInstance( // on a new instance of the interface over populating the existing instance implementing the interface. // This has already been done, so there's not need to check again. For dictionaries, we fill the existing // instance if there is one (which hasn't happened yet), and only create a new instance if necessary. - if (typeIsADictionaryInterface) - { - Type typeOfKey = type.GenericTypeArguments[0]; - Type typeOfValue = type.GenericTypeArguments[1]; - // Overwrite type in case it was a IReadOnlyDictionary<>. We still want to be able to bind items. - // REVIEW: What about settable IReadOnlyDictionary<> instances with an initial value? - // I think we should consider preferring copying like we do for all other collection interfaces. - type = typeof(Dictionary<,>).MakeGenericType(typeOfKey, typeOfValue); - } bindingPoint.SetValue(CreateInstance(type, config, options)); } @@ -360,12 +348,10 @@ private static void BindInstance( // At this point we know that we have a non-null bindingPoint.Value, we just have to populate the items // using the IDictionary<> or ICollection<> interfaces, or properties using reflection. Type? dictionaryInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type); - Type? dictionaryInterface2 = FindOpenGenericInterface(typeof(IReadOnlyDictionary<,>), type); - Type? di = dictionaryInterface ?? dictionaryInterface2; - if (di != null) + if (dictionaryInterface != null) { - BindConcreteDictionary(bindingPoint.Value!, di, config, options); + BindConcreteDictionary(bindingPoint.Value!, dictionaryInterface, config, options); } else { @@ -812,7 +798,7 @@ private static bool TypeIsADictionaryInterface(Type type) || genericTypeDefinition == typeof(IReadOnlyDictionary<,>); } - private static bool IsArrayCompatibleReadOnlyInterface(Type type) + private static bool IsArrayCompatibleInterface(Type type) { if (!type.IsInterface || !type.IsConstructedGenericType) { return false; } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs index d6c5dbce59045e..d4544ca367f8f1 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationCollectionBindingTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Xunit; @@ -1129,6 +1130,38 @@ public void CanBindInitializedCustomIndirectlyDerivedIEnumerableList() Assert.Equal("val1", array[3]); } + [Fact] + public void CanBindInitializedIReadOnlyDictionaryAndDoesNotMofifyTheOriginal() + { + // A field declared as IEnumerable that is instantiated with a class + // that indirectly implements IEnumerable is still bound, but with + // a new List with the original values copied over. + + var input = new Dictionary + { + {"AlreadyInitializedDictionary:existing_key_1", "overridden!"}, + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + var options = new InitializedCollectionsOptions(); + config.Bind(options); + + var array = options.AlreadyInitializedDictionary.ToArray(); + + Assert.Equal(2, array.Length); + + Assert.Equal("overridden!", options.AlreadyInitializedDictionary["existing_key_1"]); + Assert.Equal("val_2", options.AlreadyInitializedDictionary["existing_key_2"]); + + Assert.NotEqual(options.AlreadyInitializedDictionary, InitializedCollectionsOptions.ExistingDictionary); + + Assert.Equal("val_1", InitializedCollectionsOptions.ExistingDictionary["existing_key_1"]); + Assert.Equal("val_2", InitializedCollectionsOptions.ExistingDictionary["existing_key_2"]); + } + [Fact] public void CanBindUninitializedICollection() { @@ -1322,14 +1355,22 @@ private class InitializedCollectionsOptions public InitializedCollectionsOptions() { AlreadyInitializedIEnumerableInterface = ListUsedInIEnumerableFieldAndShouldNotBeTouched; + AlreadyInitializedDictionary = ExistingDictionary; } - public List ListUsedInIEnumerableFieldAndShouldNotBeTouched = new List + public List ListUsedInIEnumerableFieldAndShouldNotBeTouched = new() { "This was here too", "Don't touch me!" }; + public static ReadOnlyDictionary ExistingDictionary = new( + new Dictionary + { + {"existing_key_1", "val_1"}, + {"existing_key_2", "val_2"} + }); + public IEnumerable AlreadyInitializedIEnumerableInterface { get; set; } public IEnumerable AlreadyInitializedCustomListDerivedFromIEnumerable { get; set; } = @@ -1337,6 +1378,8 @@ public InitializedCollectionsOptions() public IEnumerable AlreadyInitializedCustomListIndirectlyDerivedFromIEnumerable { get; set; } = new CustomListIndirectlyDerivedFromIEnumerable(); + + public IReadOnlyDictionary AlreadyInitializedDictionary { get; set; } } private class CustomList : List From 72be9547069519213b766d43107da2160d8d8633 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Mon, 18 Jul 2022 23:20:00 +0100 Subject: [PATCH 28/28] Fix rebase issues --- .../src/ConfigurationBinder.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 3e0cc77fd176ab..745f322f20049f 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -844,18 +844,6 @@ private static bool TypeIsASetInterface(Type type) return null; } - Type[] interfaces = actual.GetInterfaces(); - foreach (Type interfaceType in interfaces) - { - if (interfaceType.IsGenericType && - interfaceType.GetGenericTypeDefinition() == expected) - { - return interfaceType; - } - } - return null; - } - private static List GetAllProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type) { var allProperties = new List();