Allow configuration binder to bind single elements to array (#58060)
authorMaryam Ariyan <maryam.ariyan@microsoft.com>
Wed, 25 Aug 2021 18:05:16 +0000 (14:05 -0400)
committerGitHub <noreply@github.com>
Wed, 25 Aug 2021 18:05:16 +0000 (11:05 -0700)
* Allow ConfigBinder to bind arrays to Singular elements (#57204)

* Apply feedback from PR #57204 (#57872)

Co-authored-by: vidommet <80355385+vidommet@users.noreply.github.com>
src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Microsoft.Extensions.Configuration.Binder.Tests.csproj

index f13edb1..9e6a7d3 100644 (file)
@@ -19,6 +19,10 @@ namespace Microsoft.Extensions.Configuration
         private const string TrimmingWarningMessage = "In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.";
         private const string InstanceGetTypeTrimmingWarningMessage = "Cannot statically analyze the type of instance so its members may be trimmed";
         private const string PropertyTrimmingWarningMessage = "Cannot statically analyze property.PropertyType so its members may be trimmed.";
+        private const string BindSingleElementsToArraySwitch = "Microsoft.Extensions.Configuration.BindSingleElementsToArray";
+
+        // Enable this switch by default.
+        private static bool ShouldBindSingleElementsToArray { get; } = AppContext.TryGetSwitch(BindSingleElementsToArraySwitch, out bool verifyCanBindSingleElementsToArray) ? verifyCanBindSingleElementsToArray : true;
 
         /// <summary>
         /// Attempts to bind the configuration instance to a new instance of type T.
@@ -362,7 +366,7 @@ namespace Microsoft.Extensions.Configuration
                 return convertedValue;
             }
 
-            if (config != null && config.GetChildren().Any())
+            if (config != null && (config.GetChildren().Any() || (configValue != null && ShouldBindSingleElementsToArray)))
             {
                 // If we don't have an instance, try to create one
                 if (instance == null)
@@ -495,7 +499,7 @@ namespace Microsoft.Extensions.Configuration
             Type itemType = collectionType.GenericTypeArguments[0];
             MethodInfo addMethod = collectionType.GetMethod("Add", DeclaredOnlyLookup);
 
-            foreach (IConfigurationSection section in config.GetChildren())
+            foreach (IConfigurationSection section in GetChildrenOrSelf(config))
             {
                 try
                 {
@@ -518,7 +522,7 @@ namespace Microsoft.Extensions.Configuration
         [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the Array so its members may be trimmed.")]
         private static Array BindArray(Array source, IConfiguration config, BinderOptions options)
         {
-            IConfigurationSection[] children = config.GetChildren().ToArray();
+            IConfigurationSection[] children = GetChildrenOrSelf(config).ToArray();
             int arrayLength = source.Length;
             Type elementType = source.GetType().GetElementType();
             var newArray = Array.CreateInstance(elementType, arrayLength + children.Length);
@@ -702,5 +706,26 @@ namespace Microsoft.Extensions.Configuration
 
             return property.Name;
         }
+
+        private static IEnumerable<IConfigurationSection> GetChildrenOrSelf(IConfiguration config)
+        {
+            if (!ShouldBindSingleElementsToArray)
+            {
+                return config.GetChildren();
+            }
+
+            IEnumerable<IConfigurationSection> children;
+            // If configuration's children is an array, the configuration key will be a number
+            if (config.GetChildren().Any(a => long.TryParse(a.Key, out _)))
+            {
+                children = config.GetChildren();
+            }
+            else
+            {
+                children = new[] { config as IConfigurationSection };
+            }
+
+            return children;
+        }
     }
-}
+}
\ No newline at end of file
index c5dbd9d..1c9e891 100644 (file)
@@ -5,6 +5,7 @@ using System;
 using System.Collections.Generic;
 using System.ComponentModel;
 using System.Reflection;
+using Microsoft.DotNet.RemoteExecutor;
 using Xunit;
 
 namespace Microsoft.Extensions.Configuration.Binder.Test
@@ -936,6 +937,59 @@ namespace Microsoft.Extensions.Configuration.Binder.Test
                 exception.Message);
         }
 
+        [Fact]
+        public void CanBindSingleElementToCollection()
+        {
+            var dic = new Dictionary<string, string>
+            {
+                {"MyString", "hello world"},
+                {"Nested:Integer", "11"},
+            };
+
+            var configurationBuilder = new ConfigurationBuilder();
+            configurationBuilder.AddInMemoryCollection(dic);
+            IConfiguration config = configurationBuilder.Build();
+
+            var stringArr = config.GetSection("MyString").Get<string[]>();
+            Assert.Equal("hello world", stringArr[0]);
+            Assert.Equal(1, stringArr.Length);
+
+            var stringAsStr = config.GetSection("MyString").Get<string>();
+            Assert.Equal("hello world", stringAsStr);
+
+            var nested = config.GetSection("Nested").Get<NestedOptions>();
+            Assert.Equal(11, nested.Integer);
+
+            var nestedAsArray = config.GetSection("Nested").Get<NestedOptions[]>();
+            Assert.Equal(11, nestedAsArray[0].Integer);
+            Assert.Equal(1, nestedAsArray.Length);
+        }
+
+        [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))]
+        public void CannotBindSingleElementToCollectionWhenSwitchSet()
+        {
+            RemoteExecutor.Invoke(() =>
+            {
+                AppContext.SetSwitch("Microsoft.Extensions.Configuration.BindSingleElementsToArray", false);
+
+                var dic = new Dictionary<string, string>
+                {
+                    {"MyString", "hello world"},
+                    {"Nested:Integer", "11"},
+                };
+
+                var configurationBuilder = new ConfigurationBuilder();
+                configurationBuilder.AddInMemoryCollection(dic);
+                IConfiguration config = configurationBuilder.Build();
+
+                var stringArr = config.GetSection("MyString").Get<string[]>();
+                Assert.Null(stringArr);
+
+                var stringAsStr = config.GetSection("MyString").Get<string>();
+                Assert.Equal("hello world", stringAsStr);
+            }).Dispose();
+        }
+
         private interface ISomeInterface
         {
         }