Issue 36015 new BinderOption to throw on missing configuration (#53852)
authorSteve Dunn <steve@dunnhq.com>
Thu, 17 Jun 2021 04:15:52 +0000 (05:15 +0100)
committerGitHub <noreply@github.com>
Thu, 17 Jun 2021 04:15:52 +0000 (21:15 -0700)
src/libraries/Microsoft.Extensions.Configuration.Binder/ref/Microsoft.Extensions.Configuration.Binder.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/src/BinderOptions.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs
src/libraries/Microsoft.Extensions.Configuration.Binder/src/Resources/Strings.resx
src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs

index 3022b1f..30cdeda 100644 (file)
@@ -10,6 +10,7 @@ namespace Microsoft.Extensions.Configuration
     {
         public BinderOptions() { }
         public bool BindNonPublicProperties { get { throw null; } set { } }
+        public bool ErrorOnUnknownConfiguration { get { throw null; } set { } }
     }
     public static partial class ConfigurationBinder
     {
index 56c3ce5..f5ffd6e 100644 (file)
@@ -13,5 +13,13 @@ namespace Microsoft.Extensions.Configuration
         /// If true, the binder will attempt to set all non read-only properties.
         /// </summary>
         public bool BindNonPublicProperties { get; set; }
+
+        /// <summary>
+        /// When false (the default), no exceptions are thrown when a configuration key is found for which the
+        /// provided model object does not have an appropriate property which matches the key's name.
+        /// When true, an <see cref="System.InvalidOperationException"/> is thrown with a description
+        /// of the missing properties.
+        /// </summary>
+        public bool ErrorOnUnknownConfiguration { get; set; }
     }
 }
index b475fb7..f13edb1 100644 (file)
@@ -209,7 +209,28 @@ namespace Microsoft.Extensions.Configuration
         {
             if (instance != null)
             {
-                foreach (PropertyInfo property in GetAllProperties(instance.GetType()))
+                List<PropertyInfo> modelProperties = GetAllProperties(instance.GetType());
+
+                if (options.ErrorOnUnknownConfiguration)
+                {
+                    HashSet<string> propertyNames = new(modelProperties.Select(mp => mp.Name),
+                        StringComparer.OrdinalIgnoreCase);
+
+                    IEnumerable<IConfigurationSection> configurationSections = configuration.GetChildren();
+                    List<string> missingPropertyNames = configurationSections
+                        .Where(cs => !propertyNames.Contains(cs.Key))
+                        .Select(mp => $"'{mp.Key}'")
+                        .ToList();
+
+                    if (missingPropertyNames.Count > 0)
+                    {
+                        throw new InvalidOperationException(SR.Format(SR.Error_MissingConfig,
+                            nameof(options.ErrorOnUnknownConfiguration), nameof(BinderOptions), instance.GetType(),
+                            string.Join(", ", missingPropertyNames)));
+                    }
+                }
+
+                foreach (PropertyInfo property in modelProperties)
                 {
                     BindProperty(property, instance, configuration, options);
                 }
@@ -622,7 +643,7 @@ namespace Microsoft.Extensions.Configuration
             return null;
         }
 
-        private static IEnumerable<PropertyInfo> GetAllProperties(
+        private static List<PropertyInfo> GetAllProperties(
             [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
             Type type)
         {
index 54660f2..dca7cc1 100644 (file)
@@ -1,17 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <root>
-  <!--
-    Microsoft ResX Schema
-
+  <!-- 
+    Microsoft ResX Schema 
+    
     Version 2.0
-
-    The primary goals of this format is to allow a simple XML format
-    that is mostly human readable. The generation and parsing of the
-    various data types are done through the TypeConverter classes
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
     associated with the data types.
-
+    
     Example:
-
+    
     ... ado.net/XML headers & schema ...
     <resheader name="resmimetype">text/microsoft-resx</resheader>
     <resheader name="version">2.0</resheader>
         <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
         <comment>This is a comment</comment>
     </data>
-
-    There are any number of "resheader" rows that contain simple
+                
+    There are any number of "resheader" rows that contain simple 
     name/value pairs.
-
-    Each data row contains a name, and value. The row also contains a
-    type or mimetype. Type corresponds to a .NET class that support
-    text/value conversion through the TypeConverter architecture.
-    Classes that don't support this are serialized and stored with the
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
     mimetype set.
-
-    The mimetype is used for serialized objects, and tells the
-    ResXResourceReader how to depersist the object. This is currently not
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
     extensible. For a given mimetype the value must be set accordingly:
-
-    Note - application/x-microsoft.net.object.binary.base64 is the format
-    that the ResXResourceWriter will generate, however the reader can
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
     read any of the formats listed below.
-
+    
     mimetype: application/x-microsoft.net.object.binary.base64
-    value   : The object must be serialized with
+    value   : The object must be serialized with 
             : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
             : and then encoded with base64 encoding.
-
+    
     mimetype: application/x-microsoft.net.object.soap.base64
-    value   : The object must be serialized with
+    value   : The object must be serialized with 
             : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
             : and then encoded with base64 encoding.
 
     mimetype: application/x-microsoft.net.object.bytearray.base64
-    value   : The object must be serialized into a byte array
+    value   : The object must be serialized into a byte array 
             : using a System.ComponentModel.TypeConverter
             : and then encoded with base64 encoding.
     -->
   <data name="Error_FailedToActivate" xml:space="preserve">
     <value>Failed to create instance of type '{0}'.</value>
   </data>
+  <data name="Error_MissingConfig" xml:space="preserve">
+    <value>'{0}' was set on the provided {1}, but the following properties were not found on the instance of {2}: {3}</value>
+  </data>
   <data name="Error_MissingParameterlessConstructor" xml:space="preserve">
     <value>Cannot create instance of type '{0}' because it is missing a public parameterless constructor.</value>
   </data>
   <data name="Error_UnsupportedMultidimensionalArray" xml:space="preserve">
     <value>Cannot create instance of type '{0}' because multidimensional arrays are not supported.</value>
   </data>
-</root>
+</root>
\ No newline at end of file
index 734bcf3..d4014ad 100644 (file)
@@ -295,6 +295,55 @@ namespace Microsoft.Extensions.Configuration.Binder.Test
         }
 
         [Fact]
+        public void ThrowsIfPropertyInConfigMissingInModel()
+        {
+            var dic = new Dictionary<string, string>
+            {
+                {"ThisDoesNotExistInTheModel", "42"},
+                {"Integer", "-2"},
+                {"Boolean", "TRUe"},
+                {"Nested:Integer", "11"}
+            };
+            var configurationBuilder = new ConfigurationBuilder();
+            configurationBuilder.AddInMemoryCollection(dic);
+            var config = configurationBuilder.Build();
+
+            var instance = new ComplexOptions();
+
+            var ex = Assert.Throws<InvalidOperationException>(
+                () => config.Bind(instance, o => o.ErrorOnUnknownConfiguration = true));
+
+            string expectedMessage = SR.Format(SR.Error_MissingConfig,
+                nameof(BinderOptions.ErrorOnUnknownConfiguration), nameof(BinderOptions), typeof(ComplexOptions), "'ThisDoesNotExistInTheModel'");
+
+            Assert.Equal(expectedMessage, ex.Message);
+        }
+        [Fact]
+        public void ThrowsIfPropertyInConfigMissingInNestedModel()
+        {
+            var dic = new Dictionary<string, string>
+            {
+                {"Nested:ThisDoesNotExistInTheModel", "42"},
+                {"Integer", "-2"},
+                {"Boolean", "TRUe"},
+                {"Nested:Integer", "11"}
+            };
+            var configurationBuilder = new ConfigurationBuilder();
+            configurationBuilder.AddInMemoryCollection(dic);
+            var config = configurationBuilder.Build();
+
+            var instance = new ComplexOptions();
+
+            string expectedMessage = SR.Format(SR.Error_MissingConfig,
+                nameof(BinderOptions.ErrorOnUnknownConfiguration), nameof(BinderOptions), typeof(NestedOptions), "'ThisDoesNotExistInTheModel'");
+
+            var ex = Assert.Throws<InvalidOperationException>(
+                () => config.Bind(instance, o => o.ErrorOnUnknownConfiguration = true));
+
+            Assert.Equal(expectedMessage, ex.Message);
+        }
+
+        [Fact]
         public void GetDefaultsWhenDataDoesNotExist()
         {
             var dic = new Dictionary<string, string>