From 5847a719c9fb4a178131beca31a3db3ebd468c11 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Tue, 8 Jun 2021 07:34:33 +0100 Subject: [PATCH 01/10] Implementation --- ...crosoft.Extensions.Configuration.Binder.cs | 36 ++++++++--------- .../src/BinderOptions.cs | 7 ++++ .../src/BindingException.cs | 30 ++++++++++++++ .../src/ConfigurationBinder.cs | 26 ++++++++++++- .../tests/ConfigurationBinderTests.cs | 39 +++++++++++++++++++ 5 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/src/BindingException.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/ref/Microsoft.Extensions.Configuration.Binder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/ref/Microsoft.Extensions.Configuration.Binder.cs index 3022b1f9c2ff70..46ed5171f6c8d7 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/ref/Microsoft.Extensions.Configuration.Binder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/ref/Microsoft.Extensions.Configuration.Binder.cs @@ -10,30 +10,30 @@ public partial class BinderOptions { public BinderOptions() { } public bool BindNonPublicProperties { get { throw null; } set { } } + public bool ErrorOnUnknownConfiguration { get { throw null; } set { } } } public static partial class ConfigurationBinder { - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Cannot statically analyze the type of instance so its members may be trimmed")] public static void Bind(this Microsoft.Extensions.Configuration.IConfiguration configuration, object instance) { } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Cannot statically analyze the type of instance so its members may be trimmed")] public static void Bind(this Microsoft.Extensions.Configuration.IConfiguration configuration, object instance, System.Action configureOptions) { } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Cannot statically analyze the type of instance so its members may be trimmed")] public static void Bind(this Microsoft.Extensions.Configuration.IConfiguration configuration, string key, object instance) { } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] public static object Get(this Microsoft.Extensions.Configuration.IConfiguration configuration, System.Type type) { throw null; } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] - public static object Get(this Microsoft.Extensions.Configuration.IConfiguration configuration, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] System.Type type, System.Action configureOptions) { throw null; } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] - public static object GetValue(this Microsoft.Extensions.Configuration.IConfiguration configuration, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] System.Type type, string key) { throw null; } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] - public static object GetValue(this Microsoft.Extensions.Configuration.IConfiguration configuration, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] System.Type type, string key, object defaultValue) { throw null; } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] - public static T GetValue<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] T>(this Microsoft.Extensions.Configuration.IConfiguration configuration, string key) { throw null; } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] - public static T GetValue<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] T>(this Microsoft.Extensions.Configuration.IConfiguration configuration, string key, T defaultValue) { throw null; } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] - public static T Get<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] T>(this Microsoft.Extensions.Configuration.IConfiguration configuration) { throw null; } - [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] - public static T Get<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] T>(this Microsoft.Extensions.Configuration.IConfiguration configuration, System.Action configureOptions) { throw null; } + public static object Get(this Microsoft.Extensions.Configuration.IConfiguration configuration, System.Type type, System.Action configureOptions) { throw null; } + public static object GetValue(this Microsoft.Extensions.Configuration.IConfiguration configuration, System.Type type, string key) { throw null; } + public static object GetValue(this Microsoft.Extensions.Configuration.IConfiguration configuration, System.Type type, string key, object defaultValue) { throw null; } + public static T GetValue(this Microsoft.Extensions.Configuration.IConfiguration configuration, string key) { throw null; } + public static T GetValue(this Microsoft.Extensions.Configuration.IConfiguration configuration, string key, T defaultValue) { throw null; } + public static T Get(this Microsoft.Extensions.Configuration.IConfiguration configuration) { throw null; } + public static T Get(this Microsoft.Extensions.Configuration.IConfiguration configuration, System.Action configureOptions) { throw null; } + } +} +namespace Microsoft.Extensions.Configuration.Binder +{ + public partial class BindingException : System.Exception + { + public BindingException() { } + protected BindingException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } + public BindingException(string message) { } + public BindingException(string message, System.Exception inner) { } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BinderOptions.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BinderOptions.cs index 56c3ce55e21b57..d66047e28f6fa2 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BinderOptions.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BinderOptions.cs @@ -13,5 +13,12 @@ public class BinderOptions /// If true, the binder will attempt to set all non read-only properties. /// public bool BindNonPublicProperties { get; set; } + + /// + /// When true, the binder will throw an exception when a configuration key is found for which the + /// provided model object does not have an appropriate property which matches + /// the key's name. + /// + public bool ErrorOnUnknownConfiguration { get; set; } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BindingException.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BindingException.cs new file mode 100644 index 00000000000000..d96e2ac1118fb7 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BindingException.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.Serialization; + +namespace Microsoft.Extensions.Configuration.Binder +{ + [Serializable] + public class BindingException : Exception + { + public BindingException() + { + } + + public BindingException(string message) : base(message) + { + } + + public BindingException(string message, Exception inner) : base(message, inner) + { + } + + protected BindingException( + SerializationInfo info, + StreamingContext context) : base(info, context) + { + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index b475fb7554fa13..0960d7a783bb29 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using Microsoft.Extensions.Configuration.Binder; namespace Microsoft.Extensions.Configuration { @@ -209,7 +211,29 @@ private static void BindNonScalar(this IConfiguration configuration, object inst { if (instance != null) { - foreach (PropertyInfo property in GetAllProperties(instance.GetType())) + IEnumerable modelProperties = GetAllProperties(instance.GetType()); + + var configSections = configuration.GetChildren(); + + if (options.ErrorOnUnknownConfiguration) + { + HashSet propertyNames = new HashSet(modelProperties.Select(mp => mp.Name), + StringComparer.OrdinalIgnoreCase); + + var missingPropertyNames = + configSections.Where(cs => !propertyNames.Contains(cs.Key)) + .Select(mp => mp.Key) + .ToList(); + + if (missingPropertyNames.Any()) + { + throw new BindingException( + $"\"{nameof(options.ErrorOnUnknownConfiguration)}\" was set on the provided {nameof(BinderOptions)}, but the following properties were not found on the instance of {instance.GetType()}:" + + Environment.NewLine + string.Join($"{Environment.NewLine}\t", missingPropertyNames)); + } + } + + foreach (PropertyInfo property in modelProperties) { BindProperty(property, instance, configuration, options); } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 734bcf3ed4aca5..e8f1aa85e86e8e 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -294,6 +294,45 @@ public void GetNullValue() Assert.Null(config.GetSection("Object").Get()); } + [Fact] + public void ThrowsIfPropertyInConfigMissingInModel() + { + var dic = new Dictionary + { + {"ThisDoesNotExistInTheModel", "42"}, + {"Integer", "-2"}, + {"Boolean", "TRUe"}, + {"Nested:Integer", "11"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var instance = new ComplexOptions(); + + Assert.Throws( + () => config.Bind(instance, o => o.ErrorOnUnknownConfiguration = true)); + } + [Fact] + public void ThrowsIfPropertyInConfigMissingInNestedModel() + { + var dic = new Dictionary + { + {"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(); + + Assert.Throws( + () => config.Bind(instance, o => o.ErrorOnUnknownConfiguration = true)); + } + [Fact] public void GetDefaultsWhenDataDoesNotExist() { From b1efdb9519ca3b14068daf5040f945a5a65bb4b9 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Tue, 8 Jun 2021 07:38:58 +0100 Subject: [PATCH 02/10] Tidy up --- .../src/ConfigurationBinder.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 0960d7a783bb29..0a68c3e583bab4 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -211,19 +211,17 @@ private static void BindNonScalar(this IConfiguration configuration, object inst { if (instance != null) { - IEnumerable modelProperties = GetAllProperties(instance.GetType()); - - var configSections = configuration.GetChildren(); + var modelProperties = GetAllProperties(instance.GetType()).ToList(); if (options.ErrorOnUnknownConfiguration) { - HashSet propertyNames = new HashSet(modelProperties.Select(mp => mp.Name), + HashSet propertyNames = new(modelProperties.Select(mp => mp.Name), StringComparer.OrdinalIgnoreCase); - var missingPropertyNames = - configSections.Where(cs => !propertyNames.Contains(cs.Key)) - .Select(mp => mp.Key) - .ToList(); + var missingPropertyNames = configuration.GetChildren() + .Where(cs => !propertyNames.Contains(cs.Key)) + .Select(mp => mp.Key) + .ToList(); if (missingPropertyNames.Any()) { From 5550d3cc94b0d578c131e67c65662365d5b24586 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Tue, 8 Jun 2021 10:31:20 +0100 Subject: [PATCH 03/10] Revert some of the auto-generated ref.cs changes made by the build target. Remove the BindingException as that'd need another API review --- ...crosoft.Extensions.Configuration.Binder.cs | 35 ++++++++++--------- .../src/BindingException.cs | 30 ---------------- .../src/ConfigurationBinder.cs | 4 +-- .../tests/ConfigurationBinderTests.cs | 4 +-- 4 files changed, 21 insertions(+), 52 deletions(-) delete mode 100644 src/libraries/Microsoft.Extensions.Configuration.Binder/src/BindingException.cs diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/ref/Microsoft.Extensions.Configuration.Binder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/ref/Microsoft.Extensions.Configuration.Binder.cs index 46ed5171f6c8d7..30cdeda1b712dd 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/ref/Microsoft.Extensions.Configuration.Binder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/ref/Microsoft.Extensions.Configuration.Binder.cs @@ -14,26 +14,27 @@ public BinderOptions() { } } public static partial class ConfigurationBinder { + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Cannot statically analyze the type of instance so its members may be trimmed")] public static void Bind(this Microsoft.Extensions.Configuration.IConfiguration configuration, object instance) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Cannot statically analyze the type of instance so its members may be trimmed")] public static void Bind(this Microsoft.Extensions.Configuration.IConfiguration configuration, object instance, System.Action configureOptions) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("Cannot statically analyze the type of instance so its members may be trimmed")] public static void Bind(this Microsoft.Extensions.Configuration.IConfiguration configuration, string key, object instance) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] public static object Get(this Microsoft.Extensions.Configuration.IConfiguration configuration, System.Type type) { throw null; } - public static object Get(this Microsoft.Extensions.Configuration.IConfiguration configuration, System.Type type, System.Action configureOptions) { throw null; } - public static object GetValue(this Microsoft.Extensions.Configuration.IConfiguration configuration, System.Type type, string key) { throw null; } - public static object GetValue(this Microsoft.Extensions.Configuration.IConfiguration configuration, System.Type type, string key, object defaultValue) { throw null; } - public static T GetValue(this Microsoft.Extensions.Configuration.IConfiguration configuration, string key) { throw null; } - public static T GetValue(this Microsoft.Extensions.Configuration.IConfiguration configuration, string key, T defaultValue) { throw null; } - public static T Get(this Microsoft.Extensions.Configuration.IConfiguration configuration) { throw null; } - public static T Get(this Microsoft.Extensions.Configuration.IConfiguration configuration, System.Action configureOptions) { throw null; } - } -} -namespace Microsoft.Extensions.Configuration.Binder -{ - public partial class BindingException : System.Exception - { - public BindingException() { } - protected BindingException(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) { } - public BindingException(string message) { } - public BindingException(string message, System.Exception inner) { } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] + public static object Get(this Microsoft.Extensions.Configuration.IConfiguration configuration, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] System.Type type, System.Action configureOptions) { throw null; } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] + public static object GetValue(this Microsoft.Extensions.Configuration.IConfiguration configuration, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] System.Type type, string key) { throw null; } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] + public static object GetValue(this Microsoft.Extensions.Configuration.IConfiguration configuration, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] System.Type type, string key, object defaultValue) { throw null; } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] + public static T GetValue<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] T>(this Microsoft.Extensions.Configuration.IConfiguration configuration, string key) { throw null; } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] + public static T GetValue<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] T>(this Microsoft.Extensions.Configuration.IConfiguration configuration, string key, T defaultValue) { throw null; } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] + public static T Get<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] T>(this Microsoft.Extensions.Configuration.IConfiguration configuration) { throw null; } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("In case the type is non-primitive, the trimmer cannot statically analyze the object's type so its members may be trimmed.")] + public static T Get<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembersAttribute(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.All)] T>(this Microsoft.Extensions.Configuration.IConfiguration configuration, System.Action configureOptions) { throw null; } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BindingException.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BindingException.cs deleted file mode 100644 index d96e2ac1118fb7..00000000000000 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BindingException.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Runtime.Serialization; - -namespace Microsoft.Extensions.Configuration.Binder -{ - [Serializable] - public class BindingException : Exception - { - public BindingException() - { - } - - public BindingException(string message) : base(message) - { - } - - public BindingException(string message, Exception inner) : base(message, inner) - { - } - - protected BindingException( - SerializationInfo info, - StreamingContext context) : base(info, context) - { - } - } -} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 0a68c3e583bab4..af43ac82f950b7 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -3,12 +3,10 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; -using Microsoft.Extensions.Configuration.Binder; namespace Microsoft.Extensions.Configuration { @@ -225,7 +223,7 @@ private static void BindNonScalar(this IConfiguration configuration, object inst if (missingPropertyNames.Any()) { - throw new BindingException( + throw new InvalidOperationException( $"\"{nameof(options.ErrorOnUnknownConfiguration)}\" was set on the provided {nameof(BinderOptions)}, but the following properties were not found on the instance of {instance.GetType()}:" + Environment.NewLine + string.Join($"{Environment.NewLine}\t", missingPropertyNames)); } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index e8f1aa85e86e8e..9011e480dc9cc0 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -310,7 +310,7 @@ public void ThrowsIfPropertyInConfigMissingInModel() var instance = new ComplexOptions(); - Assert.Throws( + Assert.Throws( () => config.Bind(instance, o => o.ErrorOnUnknownConfiguration = true)); } [Fact] @@ -329,7 +329,7 @@ public void ThrowsIfPropertyInConfigMissingInNestedModel() var instance = new ComplexOptions(); - Assert.Throws( + Assert.Throws( () => config.Bind(instance, o => o.ErrorOnUnknownConfiguration = true)); } From 8d6dcc48d6a455be6b10b4fe8a8b28290bf3a77d Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Tue, 8 Jun 2021 15:12:41 +0100 Subject: [PATCH 04/10] Doc update --- .../src/BinderOptions.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BinderOptions.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BinderOptions.cs index d66047e28f6fa2..f497741c678f8d 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BinderOptions.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BinderOptions.cs @@ -15,9 +15,10 @@ public class BinderOptions public bool BindNonPublicProperties { get; set; } /// - /// When true, the binder will throw an exception 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 false (the default), no exceptios 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 is thrown with a description + /// of the missing properties. /// public bool ErrorOnUnknownConfiguration { get; set; } } From 03d8643a81e108d0bbf8474ca7dc289ccd648a35 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Fri, 11 Jun 2021 23:11:50 +0100 Subject: [PATCH 05/10] Use string resources instead of literals --- .../src/ConfigurationBinder.cs | 8 +-- .../src/Resources/Strings.resx | 59 ++++++++++--------- 2 files changed, 35 insertions(+), 32 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index af43ac82f950b7..f6d62fbe029902 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -218,14 +218,14 @@ private static void BindNonScalar(this IConfiguration configuration, object inst var missingPropertyNames = configuration.GetChildren() .Where(cs => !propertyNames.Contains(cs.Key)) - .Select(mp => mp.Key) + .Select(mp => $"'{mp.Key}'") .ToList(); if (missingPropertyNames.Any()) { - throw new InvalidOperationException( - $"\"{nameof(options.ErrorOnUnknownConfiguration)}\" was set on the provided {nameof(BinderOptions)}, but the following properties were not found on the instance of {instance.GetType()}:" + - Environment.NewLine + string.Join($"{Environment.NewLine}\t", missingPropertyNames)); + throw new InvalidOperationException(SR.Format(SR.Error_MissingConfig, + nameof(options.ErrorOnUnknownConfiguration), nameof(BinderOptions), instance.GetType(), + string.Join(", ", missingPropertyNames))); } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Resources/Strings.resx index 54660f24f758d6..dca7cc1bf353b4 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Resources/Strings.resx @@ -1,17 +1,17 @@  - @@ -126,10 +126,13 @@ Failed to create instance of type '{0}'. + + '{0}' was set on the provided {1}, but the following properties were not found on the instance of {2}: {3} + Cannot create instance of type '{0}' because it is missing a public parameterless constructor. Cannot create instance of type '{0}' because multidimensional arrays are not supported. - + \ No newline at end of file From 89de0f35ea9cb0f2d03f3949c215e55e88fb86ed Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Sat, 12 Jun 2021 16:43:56 +0100 Subject: [PATCH 06/10] PR feedback --- .../src/BinderOptions.cs | 2 +- .../src/ConfigurationBinder.cs | 4 ++-- .../src/Resources/Strings.resx | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BinderOptions.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BinderOptions.cs index f497741c678f8d..f5ffd6eb5758f8 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BinderOptions.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/BinderOptions.cs @@ -15,7 +15,7 @@ public class BinderOptions public bool BindNonPublicProperties { get; set; } /// - /// When false (the default), no exceptios are thrown when a configuration key is found for which the + /// 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 is thrown with a description /// of the missing properties. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index f6d62fbe029902..6216b236d40fae 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -209,14 +209,14 @@ private static void BindNonScalar(this IConfiguration configuration, object inst { if (instance != null) { - var modelProperties = GetAllProperties(instance.GetType()).ToList(); + List modelProperties = GetAllProperties(instance.GetType()).ToList(); if (options.ErrorOnUnknownConfiguration) { HashSet propertyNames = new(modelProperties.Select(mp => mp.Name), StringComparer.OrdinalIgnoreCase); - var missingPropertyNames = configuration.GetChildren() + List missingPropertyNames = configuration.GetChildren() .Where(cs => !propertyNames.Contains(cs.Key)) .Select(mp => $"'{mp.Key}'") .ToList(); diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Primitives/src/Resources/Strings.resx index 197852e21f0f9f..1720724dba430e 100644 --- a/src/libraries/Microsoft.Extensions.Primitives/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Extensions.Primitives/src/Resources/Strings.resx @@ -132,4 +132,7 @@ Entire reserved capacity was not used. Capacity: '{0}', written '{1}'. + + '{0}' was set on the provided {1}, but the following properties were not found on the instance of {2}: {3} + \ No newline at end of file From 75a5f4801bf812bec65fb06e29cda1d95fc2984b Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Mon, 14 Jun 2021 19:46:37 +0100 Subject: [PATCH 07/10] PR feedback --- .../tests/ConfigurationBinderTests.cs | 15 +++++++++++++-- .../src/Resources/Strings.resx | 3 --- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 9011e480dc9cc0..47d19c43b2c3e3 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -310,8 +310,13 @@ public void ThrowsIfPropertyInConfigMissingInModel() var instance = new ComplexOptions(); - Assert.Throws( + var ex = Assert.Throws( () => 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() @@ -329,8 +334,14 @@ public void ThrowsIfPropertyInConfigMissingInNestedModel() var instance = new ComplexOptions(); - Assert.Throws( + string expectedMessage = SR.Format(SR.Error_MissingConfig, + nameof(BinderOptions.ErrorOnUnknownConfiguration), nameof(BinderOptions), typeof(NestedOptions), "'ThisDoesNotExistInTheModel'"); + + var ex = Assert.Throws( () => config.Bind(instance, o => o.ErrorOnUnknownConfiguration = true)); + + + Assert.Equal(expectedMessage, ex.Message); } [Fact] diff --git a/src/libraries/Microsoft.Extensions.Primitives/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Primitives/src/Resources/Strings.resx index 1720724dba430e..197852e21f0f9f 100644 --- a/src/libraries/Microsoft.Extensions.Primitives/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Extensions.Primitives/src/Resources/Strings.resx @@ -132,7 +132,4 @@ Entire reserved capacity was not used. Capacity: '{0}', written '{1}'. - - '{0}' was set on the provided {1}, but the following properties were not found on the instance of {2}: {3} - \ No newline at end of file From 38928052a6c95ff32f2bc67b1b5ec2e344c1e6ad Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Tue, 15 Jun 2021 06:11:59 +0100 Subject: [PATCH 08/10] PR feedback --- .../src/ConfigurationBinder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 6216b236d40fae..c8dc2b34126c56 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -221,7 +221,7 @@ private static void BindNonScalar(this IConfiguration configuration, object inst .Select(mp => $"'{mp.Key}'") .ToList(); - if (missingPropertyNames.Any()) + if (missingPropertyNames.Count > 0) { throw new InvalidOperationException(SR.Format(SR.Error_MissingConfig, nameof(options.ErrorOnUnknownConfiguration), nameof(BinderOptions), instance.GetType(), From cb0e155873ce2e6f84129d3278ebb2205ef478ea Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Wed, 16 Jun 2021 17:29:00 +0100 Subject: [PATCH 09/10] PR feedback --- .../src/ConfigurationBinder.cs | 7 ++++--- .../tests/ConfigurationBinderTests.cs | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index c8dc2b34126c56..c65f2c93bfab1c 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -209,14 +209,15 @@ private static void BindNonScalar(this IConfiguration configuration, object inst { if (instance != null) { - List modelProperties = GetAllProperties(instance.GetType()).ToList(); + List modelProperties = GetAllProperties(instance.GetType()); if (options.ErrorOnUnknownConfiguration) { HashSet propertyNames = new(modelProperties.Select(mp => mp.Name), StringComparer.OrdinalIgnoreCase); - List missingPropertyNames = configuration.GetChildren() + IEnumerable configurationSections = configuration.GetChildren().ToList(); + List missingPropertyNames = configurationSections .Where(cs => !propertyNames.Contains(cs.Key)) .Select(mp => $"'{mp.Key}'") .ToList(); @@ -642,7 +643,7 @@ private static Type FindOpenGenericInterface( return null; } - private static IEnumerable GetAllProperties( + private static List GetAllProperties( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type) { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 47d19c43b2c3e3..d4014ad734fe92 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -340,7 +340,6 @@ public void ThrowsIfPropertyInConfigMissingInNestedModel() var ex = Assert.Throws( () => config.Bind(instance, o => o.ErrorOnUnknownConfiguration = true)); - Assert.Equal(expectedMessage, ex.Message); } From dd7929e38d727b75ac9f59a063176f0f4d4309a0 Mon Sep 17 00:00:00 2001 From: Santiago Fernandez Madero Date: Wed, 16 Jun 2021 16:53:15 -0700 Subject: [PATCH 10/10] Remove ToList() call. --- .../src/ConfigurationBinder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index c65f2c93bfab1c..f13edb1872ebee 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -216,7 +216,7 @@ private static void BindNonScalar(this IConfiguration configuration, object inst HashSet propertyNames = new(modelProperties.Select(mp => mp.Name), StringComparer.OrdinalIgnoreCase); - IEnumerable configurationSections = configuration.GetChildren().ToList(); + IEnumerable configurationSections = configuration.GetChildren(); List missingPropertyNames = configurationSections .Where(cs => !propertyNames.Contains(cs.Key)) .Select(mp => $"'{mp.Key}'")