diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 963d4aa39573b0..a726a291b4567b 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -95,7 +95,7 @@ public static class ConfigurationBinder var options = new BinderOptions(); configureOptions?.Invoke(options); var bindingPoint = new BindingPoint(); - BindInstance(type, bindingPoint, config: configuration, options: options); + BindInstance(type, bindingPoint, config: configuration, options: options, isParentCollection: false); return bindingPoint.Value; } @@ -137,7 +137,7 @@ public static void Bind(this IConfiguration configuration, object? instance, Act var options = new BinderOptions(); configureOptions?.Invoke(options); var bindingPoint = new BindingPoint(instance, isReadOnly: true); - BindInstance(instance.GetType(), bindingPoint, configuration, options); + BindInstance(instance.GetType(), bindingPoint, configuration, options, false); } } @@ -260,7 +260,8 @@ private static void BindProperty(PropertyInfo property, object instance, IConfig property.PropertyType, propertyBindingPoint, config.GetSection(GetPropertyName(property)), - options); + options, + false); // For property binding, there are some cases when HasNewValue is not set in BindingPoint while a non-null Value inside that object can be retrieved from the property getter. // As example, when binding a property which not having a configuration entry matching this property and the getter can initialize the Value. @@ -277,7 +278,8 @@ private static void BindInstance( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type, BindingPoint bindingPoint, IConfiguration config, - BinderOptions options) + BinderOptions options, + bool isParentCollection) { // if binding IConfigurationSection, break early if (type == typeof(IConfigurationSection)) @@ -300,112 +302,122 @@ private static void BindInstance( return; } - if (config != null && config.GetChildren().Any()) + if (config != null) { - // for arrays and read-only list-like interfaces, we concatenate on to what is already there, if we can - if (type.IsArray || IsImmutableArrayCompatibleInterface(type)) + if (config.GetChildren().Any()) { - if (!bindingPoint.IsReadOnly) + // for arrays and read-only list-like interfaces, we concatenate on to what is already there, if we can + if (type.IsArray || IsImmutableArrayCompatibleInterface(type)) { - bindingPoint.SetValue(BindArray(type, (IEnumerable?)bindingPoint.Value, config, options)); - } + if (!bindingPoint.IsReadOnly) + { + bindingPoint.SetValue(BindArray(type, (IEnumerable?)bindingPoint.Value, config, options)); + } - // for getter-only collection properties that we can't add to, nothing more we can do - return; - } + // for getter-only collection properties that we can't add to, nothing more we can do + return; + } - // ----------------------------------------------------------------------------------------------------------------------------- - // | bindingPoint | bindingPoint | - // Interface | Value | IsReadOnly | Behavior - // ----------------------------------------------------------------------------------------------------------------------------- - // ISet | not null | true/false | Use the Value instance to populate the configuration - // ISet | null | false | Create HashSet instance to populate the configuration - // ISet | null | true | nothing - // IReadOnlySet | null/not null | false | Create HashSet instance, copy over existing values, and populate the configuration - // IReadOnlySet | null/not null | true | nothing - // ----------------------------------------------------------------------------------------------------------------------------- - if (TypeIsASetInterface(type)) - { - if (!bindingPoint.IsReadOnly || bindingPoint.Value is not null) + // ----------------------------------------------------------------------------------------------------------------------------- + // | bindingPoint | bindingPoint | + // Interface | Value | IsReadOnly | Behavior + // ----------------------------------------------------------------------------------------------------------------------------- + // ISet | not null | true/false | Use the Value instance to populate the configuration + // ISet | null | false | Create HashSet instance to populate the configuration + // ISet | null | true | nothing + // IReadOnlySet | null/not null | false | Create HashSet instance, copy over existing values, and populate the configuration + // IReadOnlySet | null/not null | true | nothing + // ----------------------------------------------------------------------------------------------------------------------------- + if (TypeIsASetInterface(type)) { - object? newValue = BindSet(type, (IEnumerable?)bindingPoint.Value, config, options); - if (!bindingPoint.IsReadOnly && newValue != null) + if (!bindingPoint.IsReadOnly || bindingPoint.Value is not null) { - bindingPoint.SetValue(newValue); + object? newValue = BindSet(type, (IEnumerable?)bindingPoint.Value, config, options); + if (!bindingPoint.IsReadOnly && newValue != null) + { + bindingPoint.SetValue(newValue); + } } - } - return; - } + return; + } - // ----------------------------------------------------------------------------------------------------------------------------- - // | bindingPoint | bindingPoint | - // Interface | Value | IsReadOnly | Behavior - // ----------------------------------------------------------------------------------------------------------------------------- - // IDictionary | not null | true/false | Use the Value instance to populate the configuration - // IDictionary | null | false | Create Dictionary instance to populate the configuration - // IDictionary | null | true | nothing - // IReadOnlyDictionary | null/not null | false | Create Dictionary instance, copy over existing values, and populate the configuration - // IReadOnlyDictionary | null/not null | true | nothing - // ----------------------------------------------------------------------------------------------------------------------------- - if (TypeIsADictionaryInterface(type)) - { - if (!bindingPoint.IsReadOnly || bindingPoint.Value is not null) + // ----------------------------------------------------------------------------------------------------------------------------- + // | bindingPoint | bindingPoint | + // Interface | Value | IsReadOnly | Behavior + // ----------------------------------------------------------------------------------------------------------------------------- + // IDictionary | not null | true/false | Use the Value instance to populate the configuration + // IDictionary | null | false | Create Dictionary instance to populate the configuration + // IDictionary | null | true | nothing + // IReadOnlyDictionary | null/not null | false | Create Dictionary instance, copy over existing values, and populate the configuration + // IReadOnlyDictionary | null/not null | true | nothing + // ----------------------------------------------------------------------------------------------------------------------------- + if (TypeIsADictionaryInterface(type)) { - object? newValue = BindDictionaryInterface(bindingPoint.Value, type, config, options); - if (!bindingPoint.IsReadOnly && newValue != null) + if (!bindingPoint.IsReadOnly || bindingPoint.Value is not null) { - bindingPoint.SetValue(newValue); + object? newValue = BindDictionaryInterface(bindingPoint.Value, type, config, options); + if (!bindingPoint.IsReadOnly && newValue != null) + { + bindingPoint.SetValue(newValue); + } } - } - return; - } + return; + } - // If we don't have an instance, try to create one - if (bindingPoint.Value is null) - { - // if the binding point doesn't let us set a new instance, there's nothing more we can do - if (bindingPoint.IsReadOnly) + // If we don't have an instance, try to create one + if (bindingPoint.Value is null) { - return; + // if the binding point doesn't let us set a new instance, there's nothing more we can do + if (bindingPoint.IsReadOnly) + { + return; + } + + Type? interfaceGenericType = type.IsInterface && type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : null; + + if (interfaceGenericType is not null && + (interfaceGenericType == typeof(ICollection<>) || interfaceGenericType == typeof(IList<>))) + { + // For ICollection and IList we bind them to mutable List type. + Type genericType = typeof(List<>).MakeGenericType(type.GenericTypeArguments[0]); + bindingPoint.SetValue(Activator.CreateInstance(genericType)); + } + else + { + bindingPoint.SetValue(CreateInstance(type, config, options)); + } } - Type? interfaceGenericType = type.IsInterface && type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : null; + Debug.Assert(bindingPoint.Value is not 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 (interfaceGenericType is not null && - (interfaceGenericType == typeof(ICollection<>) || interfaceGenericType == typeof(IList<>))) + if (dictionaryInterface != null) { - // For ICollection and IList we bind them to mutable List type. - Type genericType = typeof(List<>).MakeGenericType(type.GenericTypeArguments[0]); - bindingPoint.SetValue(Activator.CreateInstance(genericType)); + BindDictionary(bindingPoint.Value, dictionaryInterface, config, options); } else { - bindingPoint.SetValue(CreateInstance(type, config, options)); + Type? collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type); + if (collectionInterface != null) + { + BindCollection(bindingPoint.Value, collectionInterface, config, options); + } + else + { + BindProperties(bindingPoint.Value, config, options); + } } } - - Debug.Assert(bindingPoint.Value is not 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, dictionaryInterface, config, options); - } else { - Type? collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type); - if (collectionInterface != null) - { - BindCollection(bindingPoint.Value, collectionInterface, config, options); - } - else + if (isParentCollection) { - BindProperties(bindingPoint.Value, config, options); + bindingPoint.TrySetValue(CreateInstance(type, config, options)); } } } @@ -647,7 +659,8 @@ private static void BindDictionary( type: valueType, bindingPoint: valueBindingPoint, config: child, - options: options); + options: options, + true); if (valueBindingPoint.HasNewValue) { indexerProperty.SetValue(dictionary, valueBindingPoint.Value, new object[] { key }); @@ -685,7 +698,8 @@ private static void BindCollection( type: itemType, bindingPoint: itemBindingPoint, config: section, - options: options); + options: options, + true); if (itemBindingPoint.HasNewValue) { addMethod?.Invoke(collection, new[] { itemBindingPoint.Value }); @@ -740,7 +754,8 @@ private static Array BindArray(Type type, IEnumerable? source, IConfiguration co type: elementType, bindingPoint: itemBindingPoint, config: section, - options: options); + options: options, + isParentCollection: true); if (itemBindingPoint.HasNewValue) { list.Add(itemBindingPoint.Value); @@ -808,7 +823,8 @@ private static Array BindArray(Type type, IEnumerable? source, IConfiguration co type: elementType, bindingPoint: itemBindingPoint, config: section, - options: options); + options: options, + true); if (itemBindingPoint.HasNewValue) { arguments[0] = itemBindingPoint.Value; @@ -995,7 +1011,8 @@ private static List GetAllProperties([DynamicallyAccessedMembers(D parameter.ParameterType, propertyBindingPoint, config.GetSection(parameterName), - options); + options, + false); if (propertyBindingPoint.Value is null) { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs index 8690c75f3383d7..56e03b5f768204 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text.Json.Serialization; using Microsoft.Extensions.Configuration; using Xunit; @@ -580,6 +581,25 @@ public class ClassWithIndirectSelfReference public List MyList { get; set; } } + public class DistributedQueueConfig + { + public List Namespaces { get; set; } + } + + public class QueueNamespaces + { + public string Namespace { get; set; } + + public Dictionary Queues { get; set; } = new(); + } + + public class QueueProperties + { + public DateTimeOffset? CreationDate { get; set; } + + public DateTimeOffset? DequeueOnlyMarkedDate { get; set; } = default(DateTimeOffset); + } + public record RecordWithPrimitives { public bool Prop0 { get; set; } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs index 121d9a908606eb..c84d33bf8a4d2c 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs @@ -1625,6 +1625,51 @@ public void EnsureCallingThePropertySetter() #endif } + [Fact] + public void EnsureSuccessfullyBind() + { + var json = @"{ + ""queueConfig"": { + ""Namespaces"": [ + { + ""Namespace"": ""devnortheurope"", + ""Queues"": { + ""q1"": { + ""DequeueOnlyMarkedDate"": ""2022-01-20T12:49:03.395150-08:00"" + }, + ""q2"": { + ""DequeueOnlyMarkedDate"": ""2022-01-20T12:49:03.395150-08:00"" + } + } + }, + { + ""Namespace"": ""devnortheurope2"", + ""Queues"": { + ""q3"": { + ""DequeueOnlyMarkedDate"": ""2022-01-20T12:49:03.395150-08:00"" + }, + ""q4"": { + } + } + } + ] + } + }"; + + var configuration = new ConfigurationBuilder() + .AddJsonStream(TestStreamHelpers.StringToStream(json)) + .Build(); + + DistributedQueueConfig options = new DistributedQueueConfig(); + configuration.GetSection("queueConfig").Bind(options); + + Assert.NotNull(options); + Assert.Equal(2, options.Namespaces.Count); + Assert.Equal(2, options.Namespaces.First().Queues.Count); + Assert.Equal(2, options.Namespaces.Skip(1).First().Queues.Count); + Assert.NotNull(options.Namespaces.Skip(1).First().Queues.Last().Value); + } + [Fact] public void RecursiveTypeGraphs_DirectRef() {