{
public BinderOptions() { }
public bool BindNonPublicProperties { get { throw null; } set { } }
+ public bool ErrorOnUnknownConfiguration { get { throw null; } set { } }
}
public static partial class ConfigurationBinder
{
/// 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; }
}
}
{
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);
}
return null;
}
- private static IEnumerable<PropertyInfo> GetAllProperties(
+ private static List<PropertyInfo> GetAllProperties(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
Type type)
{
<?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
}
[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>