diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 7eca0e5f41db86..745f322f20049f 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,75 +258,6 @@ 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 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; - } - - // 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( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] - Type type, - IConfiguration config, BinderOptions options) - { - if (!type.IsInterface) - { - return null; - } - - Type? collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyList<>), type); - if (collectionInterface != null) - { - // IEnumerable is guaranteed to have exactly one parameter - return BindToCollection(type, config, options); - } - - collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyDictionary<,>), type); - 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; - } - - collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type); - if (collectionInterface != null) - { - object instance = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(type.GenericTypeArguments[0], type.GenericTypeArguments[1]))!; - BindDictionary(instance, collectionInterface, config, options); - return instance; - } - - 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) - { - // ICollection is guaranteed to have exactly one parameter - return BindToCollection(type, config, options); - } - - collectionInterface = FindOpenGenericInterface(typeof(IEnumerable<>), type); - if (collectionInterface != null) - { - // IEnumerable is guaranteed to have exactly one parameter - return BindToCollection(type, config, options); - } - - return null; - } - [RequiresUnreferencedCode(TrimmingWarningMessage)] private static void BindInstance( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type, @@ -357,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) { @@ -367,6 +298,36 @@ private static void BindInstance( return; } + // for sets and read-only set interfaces, we clone what's there into a new collection. + if (TypeIsASetInterface(type)) + { + if (!bindingPoint.IsReadOnly) + { + object? newValue = BindSet(type, (IEnumerable?)bindingPoint.Value, config, options); + if (newValue != null) + { + bindingPoint.SetValue(newValue); + } + } + return; + } + + // 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. + if (TypeIsADictionaryInterface(type)) + { + if (!bindingPoint.IsReadOnly) + { + object? newValue = BindDictionaryInterface(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) { @@ -376,34 +337,32 @@ private static void BindInstance( return; } - object? boundFromInterface = AttemptBindToCollectionInterfaces(type, config, options); - if (boundFromInterface != null) - { - bindingPoint.SetValue(boundFromInterface); - return; // We are already done if binding to a new collection instance worked - } + // 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. 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); + BindConcreteDictionary(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); } } } @@ -522,8 +481,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? BindDictionaryInterface( + object? source, [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)] Type dictionaryType, IConfiguration config, BinderOptions options) @@ -533,13 +492,68 @@ private static void BindDictionary( Type valueType = dictionaryType.GenericTypeArguments[1]; bool keyTypeIsEnum = keyType.IsEnum; + if (keyType != typeof(string) && !keyTypeIsEnum) + { + // We only support string and enum keys + return null; + } + + Type genericType = typeof(Dictionary<,>).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)!; + + object dictionary = Activator.CreateInstance(genericType)!; + + var orig = source as IEnumerable; + object?[] arguments = new object?[2]; + + if (orig != null) + { + 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(dictionary, arguments); + } + } + + 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 BindConcreteDictionary( + 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 = dictionaryType.GetProperty("Item", DeclaredOnlyLookup)!; + PropertyInfo setter = genericType.GetProperty("Item", DeclaredOnlyLookup)!; foreach (IConfigurationSection child in config.GetChildren()) { try @@ -548,7 +562,7 @@ private static void BindDictionary( var valueBindingPoint = new BindingPoint( initialValueProvider: () => { - var tryGetValueArgs = new object?[] { key, null }; + object?[] tryGetValueArgs = { key, null }; return (bool)tryGetValue.Invoke(dictionary, tryGetValueArgs)! ? tryGetValueArgs[1] : null; }, isReadOnly: false); @@ -652,6 +666,62 @@ 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 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)!; + + object?[] arguments = new object?[1]; + + if (source != null) + { + foreach (object? item in source) + { + arguments[0] = item; + addMethod.Invoke(instance, arguments); + } + } + + foreach (IConfigurationSection section in config.GetChildren()) + { + var itemBindingPoint = new BindingPoint(); + try + { + BindInstance( + type: elementType, + bindingPoint: itemBindingPoint, + config: section, + options: options); + if (itemBindingPoint.HasNewValue) + { + arguments[0] = itemBindingPoint.Value; + + addMethod.Invoke(instance, arguments); + } + } + catch + { + } + } + + return instance; + } + [RequiresUnreferencedCode(TrimmingWarningMessage)] private static bool TryConvertValue( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] @@ -719,16 +789,38 @@ private static bool TryConvertValue( return result; } - private static bool IsArrayCompatibleReadOnlyInterface(Type type) + private static bool TypeIsADictionaryInterface(Type type) + { + if (!type.IsInterface || !type.IsConstructedGenericType) { return false; } + + Type genericTypeDefinition = type.GetGenericTypeDefinition(); + return genericTypeDefinition == typeof(IDictionary<,>) + || genericTypeDefinition == typeof(IReadOnlyDictionary<,>); + } + + private static bool IsArrayCompatibleInterface(Type type) { if (!type.IsInterface || !type.IsConstructedGenericType) { return false; } Type genericTypeDefinition = type.GetGenericTypeDefinition(); return genericTypeDefinition == typeof(IEnumerable<>) + || genericTypeDefinition == typeof(ICollection<>) || genericTypeDefinition == typeof(IReadOnlyCollection<>) || genericTypeDefinition == typeof(IReadOnlyList<>); } + private static bool TypeIsASetInterface(Type type) + { + if (!type.IsInterface || !type.IsConstructedGenericType) { return false; } + + Type genericTypeDefinition = type.GetGenericTypeDefinition(); + return genericTypeDefinition == typeof(ISet<>) +#if NETCOREAPP + || 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 b7795c4ccf42c2..0731ff20419873 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; @@ -14,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(); @@ -49,7 +56,57 @@ public string ReadOnly get { return null; } } + 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>(); + + 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(); + + 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(); + + public ISet UninstantiatedHashSetWithUnsupportedKey { get; set; } + +#if NETCOREAPP + public IReadOnlySet InstantiatedIReadOnlySet { get; set; } = new HashSet(); + public IReadOnlySet InstantiatedIReadOnlySetWithSomeValues { get; set; } = + new HashSet(new[] { "existing1", "existing2" }); + public IReadOnlySet NonInstantiatedIReadOnlySet { get; set; } + public IDictionary> InstantiatedDictionaryWithReadOnlySetWithSomeValues { get; set; } = + new Dictionary> + { + {"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(); @@ -75,6 +132,85 @@ public override string Virtual } } + public class UnsupportedTypeInHashSet { } + + public interface ICustomCollectionDerivedFromIEnumerableT : IEnumerable { } + public interface ICustomCollectionDerivedFromICollectionT : ICollection { } + + public class MyClassWithCustomCollections + { + public ICustomCollectionDerivedFromIEnumerableT CustomIEnumerableCollection { get; set; } + 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 + { + } + + public class MyClassWithCustomSet + { + public ICustomSet CustomSet { get; set; } + } + + 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 ConfigWithNonInstantiatedReadOnlyDictionary + { + public IReadOnlyDictionary Dictionary { get; set; } = null!; + } + + public class ConfigWithInstantiatedConcreteDictionary + { + public static Dictionary _existingDictionary = new() + { + {"existing-item1", 1}, + {"existing-item2", 2}, + }; + + public Dictionary Dictionary { get; set; } = + _existingDictionary; + } + + public interface ICustomDictionary : IDictionary + { + } + public class NullableOptions { public bool? MyNullableBool { get; set; } @@ -478,6 +614,569 @@ public void CanBindNonInstantiatedIEnumerableWithItems() Assert.Equal("Yo2", options.NonInstantiatedIEnumerable.ElementAt(1)); } + [Fact] + public void CanBindNonInstantiatedISet() + { + var dic = new Dictionary + { + {"NonInstantiatedISet:0", "Yo1"}, + {"NonInstantiatedISet:1", "Yo2"}, + {"NonInstantiatedISet:2", "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)); + } + +#if NETCOREAPP + [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 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() + { + 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)); + } + + [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 + + 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 BindInstantiatedIReadOnlyDictionary_CreatesCopyOfOriginal() + { + var dic = new Dictionary + { + {"Dictionary:existing-item1", "666"}, + {"Dictionary:item3", "3"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + Assert.Equal(3, 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"]); + } + + [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() + { + var dic = new Dictionary + { + {"Dictionary:existing-item1", "666"}, + {"Dictionary:item3", "3"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + + var config = configurationBuilder.Build(); + + var options = config.Get()!; + + 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"]); + } + + [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()!; + + 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"]); + } + + [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() + { + 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 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 ThrowsForCustomIEnumerableCollection() + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(new Dictionary + { + ["CustomIEnumerableCollection:0"] = "Yo!", + }); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Get()); + Assert.Equal( + SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomCollectionDerivedFromIEnumerableT)), + exception.Message); + } + + [Fact] + public void ThrowsForCustomICollection() + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(new Dictionary + { + ["CustomCollection:0"] = "Yo!", + }); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Get()); + Assert.Equal( + SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomCollectionDerivedFromICollectionT)), + exception.Message); + } + + [Fact] + public void ThrowsForCustomDictionary() + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(new Dictionary + { + ["CustomDictionary:0"] = "Yo!", + }); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Get()); + Assert.Equal( + SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomDictionary)), + exception.Message); + } + + [Fact] + public void ThrowsForCustomSet() + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(new Dictionary + { + ["CustomSet:0"] = "Yo!", + }); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Get()); + Assert.Equal( + SR.Format(SR.Error_CannotActivateAbstractOrInterface, typeof(ICustomSet)), + exception.Message); + } + + [Fact] + public void CanBindInstantiatedISet() + { + var dic = new Dictionary + { + {"InstantiatedISet:0", "Yo1"}, + {"InstantiatedISet:1", "Yo2"}, + {"InstantiatedISet:2", "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 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 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() + { + 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() { @@ -498,6 +1197,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() { @@ -1107,7 +1846,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..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; @@ -581,6 +582,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 +711,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() { @@ -1073,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() { @@ -1266,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; } = @@ -1281,6 +1378,8 @@ public InitializedCollectionsOptions() public IEnumerable AlreadyInitializedCustomListIndirectlyDerivedFromIEnumerable { get; set; } = new CustomListIndirectlyDerivedFromIEnumerable(); + + public IReadOnlyDictionary AlreadyInitializedDictionary { get; set; } } private class CustomList : List @@ -1410,6 +1509,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 +1522,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 +1532,7 @@ public OptionsWithDictionary() public IDictionary StringDictionaryInterface { get; set; } public IDictionary AlreadyInitializedStringDictionaryInterface { get; set; } + public IDictionary> AlreadyInitializedHashSetDictionary { get; set; } } private class OptionsWithInterdependentProperties