diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BindingPoint.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BindingPoint.cs index 95e52f444ce877..c2dec39869df21 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BindingPoint.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BindingPoint.cs @@ -27,6 +27,24 @@ public BindingPoint(Func initialValueProvider, bool isReadOnly) public bool IsReadOnly { get; } + public bool IsValueCanBeSet + { + get + { + return Value is null + && !IsReadOnly; + } + } + + public bool IsValueCanBeUpdated + { + get + { + return Value is not null + || !IsReadOnly; + } + } + public bool HasNewValue { get diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 963d4aa39573b0..a81e27f40e95ec 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -283,129 +283,21 @@ private static void BindInstance( if (type == typeof(IConfigurationSection)) { bindingPoint.TrySetValue(config); - return; } - - var section = config as IConfigurationSection; - string? configValue = section?.Value; - if (configValue != null && TryConvertValue(type, configValue, section?.Path, out object? convertedValue, out Exception? error)) + else { - if (error != null) - { - throw error; - } - - // Leaf nodes are always reinitialized - bindingPoint.TrySetValue(convertedValue); - return; - } - - if (config != null && config.GetChildren().Any()) - { - // 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 (!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; - } - - // ----------------------------------------------------------------------------------------------------------------------------- - // | 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 (TryBindAsSimpleValue(type, bindingPoint, config, out Exception? error)) { - if (!bindingPoint.IsReadOnly || bindingPoint.Value is not null) + if (error != null) { - object? newValue = BindSet(type, (IEnumerable?)bindingPoint.Value, config, options); - if (!bindingPoint.IsReadOnly && newValue != null) - { - bindingPoint.SetValue(newValue); - } + throw error; } - - 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) - { - object? newValue = BindDictionaryInterface(bindingPoint.Value, type, config, options); - if (!bindingPoint.IsReadOnly && newValue != null) - { - bindingPoint.SetValue(newValue); - } - } - - 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) - { - 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)); - } - } - - 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 (!TryBindAsCollectionValue(type, bindingPoint, config, options)) { - BindProperties(bindingPoint.Value, config, options); + bindingPoint.SetValue(CreateInstance(type, config, options)); } } } @@ -648,10 +540,7 @@ private static void BindDictionary( bindingPoint: valueBindingPoint, config: child, options: options); - if (valueBindingPoint.HasNewValue) - { - indexerProperty.SetValue(dictionary, valueBindingPoint.Value, new object[] { key }); - } + indexerProperty.SetValue(dictionary, valueBindingPoint.Value, new object[] { key }); } catch (Exception ex) { @@ -829,6 +718,160 @@ private static Array BindArray(Type type, IEnumerable? source, IConfiguration co return source; } + [RequiresDynamicCode(DynamicCodeWarningMessage)] + [RequiresUnreferencedCode(TrimmingWarningMessage)] + private static bool TryBindAsCollectionValue( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type, + BindingPoint bindingPoint, + IConfiguration config, + BinderOptions options) + { + if (config != null && config.GetChildren().Any()) + { + // 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 (!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 true; + } + + // ----------------------------------------------------------------------------------------------------------------------------- + // | 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.IsValueCanBeUpdated) + { + object? newValue = BindSet(type, (IEnumerable?)bindingPoint.Value, config, options); + if (!bindingPoint.IsReadOnly && newValue != null) + { + bindingPoint.SetValue(newValue); + } + } + + return true; + } + + // ----------------------------------------------------------------------------------------------------------------------------- + // | 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.IsValueCanBeUpdated) + { + object? newValue = BindDictionaryInterface(bindingPoint.Value, type, config, options); + if (!bindingPoint.IsReadOnly && newValue != null) + { + bindingPoint.SetValue(newValue); + } + } + + return true; + } + + if (bindingPoint.IsValueCanBeSet) + { + 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)); + } + } + + 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 + { + BindProperties(bindingPoint.Value, config, options); + } + } + + return true; + } + else + { + return false; + } + } + + [RequiresUnreferencedCode(TrimmingWarningMessage)] + private static bool TryBindAsSimpleValue( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type, + BindingPoint bindingPoint, + IConfiguration config, + out Exception? error) + { + bool returnValue; + + var section = config as IConfigurationSection; + string? configValue = section?.Value; + + if (configValue != null) + { + if (TryConvertValue(type, configValue, section?.Path, out object? convertedValue, out error)) + { + if (error == null) + { + bindingPoint.TrySetValue(convertedValue); + } + returnValue = true; + } + else + { + returnValue = false; + } + } + else + { + error = null; + returnValue = false; + } + + return returnValue; + } + [RequiresUnreferencedCode(TrimmingWarningMessage)] private static bool TryConvertValue( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] 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..fb7ac792707916 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,30 @@ public class ClassWithIndirectSelfReference public List MyList { get; set; } } + public class DistributedQueueConfig + { + [JsonPropertyName("namespaces")] + public IList Namespaces { get; set; } + } + + public class QueueNamespaces + { + [JsonPropertyName("namespace")] + public string Namespace { get; set; } + + [JsonPropertyName("queues")] + public Dictionary Queues { get; set; } = new(); + } + + public class QueueProperties + { + [JsonPropertyName("creationDate")] + public DateTimeOffset? CreationDate { get; set; } + + [JsonPropertyName("dequeueOnlyMarkedDate")] + 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..9d7750ab74f8d9 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() {