From d78f3648b45aeccdcfc198525eff1c3a2ade6e05 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Thu, 9 Jun 2022 20:14:03 +0100 Subject: [PATCH] Support immutable types with configuration binding (#67258) Merging. Thanks @SteveDunn for your contribution. --- .../src/ConfigurationBinder.cs | 138 +++- ...oft.Extensions.Configuration.Binder.csproj | 9 +- .../src/Resources/Strings.resx | 19 +- .../tests/ConfigurationBinderTests.cs | 639 +++++++++++++++++- ...tensions.Configuration.Binder.Tests.csproj | 1 + 5 files changed, 790 insertions(+), 16 deletions(-) diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index e56a0875bd46e..2d2f5658188a4 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -9,6 +9,7 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using Microsoft.Extensions.Internal; namespace Microsoft.Extensions.Configuration { @@ -328,9 +329,10 @@ private static object BindToCollection(Type type, IConfiguration config, BinderO [RequiresUnreferencedCode(TrimmingWarningMessage)] private static void BindInstance( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] - Type type, - BindingPoint bindingPoint, IConfiguration config, BinderOptions options) + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type, + BindingPoint bindingPoint, + IConfiguration config, + BinderOptions options) { // if binding IConfigurationSection, break early if (type == typeof(IConfigurationSection)) @@ -381,7 +383,7 @@ private static object BindToCollection(Type type, IConfiguration config, BinderO return; // We are already done if binding to a new collection instance worked } - bindingPoint.SetValue(CreateInstance(type)); + bindingPoint.SetValue(CreateInstance(type, config, options)); } // See if it's a Dictionary @@ -407,23 +409,63 @@ private static object BindToCollection(Type type, IConfiguration config, BinderO } } - [RequiresUnreferencedCode("In case type is a Nullable, cannot statically analyze what the underlying type is so its members may be trimmed.")] - private static object CreateInstance([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type) + [RequiresUnreferencedCode( + "In case type is a Nullable, cannot statically analyze what the underlying type is so its members may be trimmed.")] + private static object CreateInstance( + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | + DynamicallyAccessedMemberTypes.NonPublicConstructors)] + Type type, + IConfiguration config, + BinderOptions options) { Debug.Assert(!type.IsArray); - if (type.IsAbstract) + if (type.IsInterface || type.IsAbstract) { throw new InvalidOperationException(SR.Format(SR.Error_CannotActivateAbstractOrInterface, type)); } - if (!type.IsValueType) + ConstructorInfo[] constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance); + + bool hasParameterlessConstructor = + type.IsValueType || constructors.Any(ctor => ctor.GetParameters().Length == 0); + + if (!type.IsValueType && constructors.Length == 0) + { + throw new InvalidOperationException(SR.Format(SR.Error_MissingPublicInstanceConstructor, type)); + } + + if (constructors.Length > 1 && !hasParameterlessConstructor) { - bool hasDefaultConstructor = type.GetConstructors(DeclaredOnlyLookup).Any(ctor => ctor.IsPublic && ctor.GetParameters().Length == 0); - if (!hasDefaultConstructor) + throw new InvalidOperationException(SR.Format(SR.Error_MultipleParameterizedConstructors, type)); + } + + if (constructors.Length == 1 && !hasParameterlessConstructor) + { + ConstructorInfo constructor = constructors[0]; + ParameterInfo[] parameters = constructor.GetParameters(); + + if (!CanBindToTheseConstructorParameters(parameters, out string nameOfInvalidParameter)) + { + throw new InvalidOperationException(SR.Format(SR.Error_CannotBindToConstructorParameter, type, nameOfInvalidParameter)); + } + + + List properties = GetAllProperties(type); + + if (!DoAllParametersHaveEquivalentProperties(parameters, properties, out string nameOfInvalidParameters)) + { + throw new InvalidOperationException(SR.Format(SR.Error_ConstructorParametersDoNotMatchProperties, type, nameOfInvalidParameters)); + } + + object?[] parameterValues = new object?[parameters.Length]; + + for (int index = 0; index < parameters.Length; index++) { - throw new InvalidOperationException(SR.Format(SR.Error_MissingParameterlessConstructor, type)); + parameterValues[index] = BindParameter(parameters[index], type, config, options); } + + return constructor.Invoke(parameterValues); } object? instance; @@ -439,6 +481,46 @@ private static object CreateInstance([DynamicallyAccessedMembers(DynamicallyAcce return instance ?? throw new InvalidOperationException(SR.Format(SR.Error_FailedToActivate, type)); } + private static bool DoAllParametersHaveEquivalentProperties(ParameterInfo[] parameters, + List properties, out string missing) + { + HashSet propertyNames = new(StringComparer.OrdinalIgnoreCase); + foreach (PropertyInfo prop in properties) + { + propertyNames.Add(prop.Name); + } + + List missingParameters = new(); + + foreach (ParameterInfo parameter in parameters) + { + string name = parameter.Name!; + if (!propertyNames.Contains(name)) + { + missingParameters.Add(name); + } + } + + missing = string.Join(",", missingParameters); + + return missing.Length == 0; + } + + private static bool CanBindToTheseConstructorParameters(ParameterInfo[] constructorParameters, out string nameOfInvalidParameter) + { + nameOfInvalidParameter = string.Empty; + foreach (ParameterInfo p in constructorParameters) + { + if (p.IsOut || p.IsIn || p.ParameterType.IsByRef) + { + nameOfInvalidParameter = p.Name!; // never null as we're not passed return value parameters: https://docs.microsoft.com/en-us/dotnet/api/system.reflection.parameterinfo.name?view=net-6.0#remarks + return false; + } + } + + return true; + } + [RequiresUnreferencedCode("Cannot statically analyze what the element type is of the value objects in the dictionary so its members may be trimmed.")] private static void BindDictionary( object dictionary, @@ -687,6 +769,40 @@ private static bool IsArrayCompatibleReadOnlyInterface(Type type) return allProperties; } + [RequiresUnreferencedCode(PropertyTrimmingWarningMessage)] + private static object? BindParameter(ParameterInfo parameter, Type type, IConfiguration config, + BinderOptions options) + { + string? parameterName = parameter.Name; + + if (parameterName is null) + { + throw new InvalidOperationException(SR.Format(SR.Error_ParameterBeingBoundToIsUnnamed, type)); + } + + var propertyBindingPoint = new BindingPoint(initialValue: config.GetSection(parameterName).Value, isReadOnly: false); + + if (propertyBindingPoint.Value is null) + { + if (ParameterDefaultValue.TryGetDefaultValue(parameter, out object? defaultValue)) + { + propertyBindingPoint.SetValue(defaultValue); + } + else + { + throw new InvalidOperationException(SR.Format(SR.Error_ParameterHasNoMatchingConfig, type, parameterName)); + } + } + + BindInstance( + parameter.ParameterType, + propertyBindingPoint, + config.GetSection(parameterName), + options); + + return propertyBindingPoint.Value; + } + private static string GetPropertyName(MemberInfo property) { ThrowHelper.ThrowIfNull(property); diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Microsoft.Extensions.Configuration.Binder.csproj b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Microsoft.Extensions.Configuration.Binder.csproj index 747e97a01882f..d74d502d6b573 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Microsoft.Extensions.Configuration.Binder.csproj +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Microsoft.Extensions.Configuration.Binder.csproj @@ -9,14 +9,19 @@ - + + + + + + + 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 dca7cc1bf353b..197b325252ef9 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/Resources/Strings.resx @@ -120,6 +120,12 @@ Cannot create instance of type '{0}' because it is either abstract or an interface. + + Cannot create instance of type '{0}' because one or more parameters cannot be bound to. Constructor parameters cannot be declared as in, out, or ref. Invalid parameters are: '{1}' + + + Cannot create instance of type '{0}' because one or more parameters cannot be bound to. Constructor parameters must have corresponding properties. Fields are not supported. Missing properties are: '{1}' + Failed to convert configuration value at '{0}' to type '{1}'. @@ -129,8 +135,17 @@ '{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 it is missing a public instance constructor. + + + Cannot create instance of type '{0}' because it has multiple public parameterized constructors. + + + Cannot create instance of type '{0}' because one or more parameters are unnamed. + + + Cannot create instance of type '{0}' because parameter '{1}' has no matching config. Each parameter in the constructor that does not have a default value must have a corresponding config entry. Cannot create instance of type '{0}' because multidimensional arrays are not supported. diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 04e3ca5197762..932b6111fb25a 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -112,6 +112,238 @@ public class DerivedOptionsWithIConfigurationSection : DerivedOptions public IConfigurationSection DerivedSection { get; set; } } + public record struct RecordStructTypeOptions(string Color, int Length); + + // Here, the constructor has three parameters, but not all of those match + // match to a property or field + public class ClassWhereParametersDoNotMatchProperties + { + public string Name { get; } + public string Address { get; } + + public ClassWhereParametersDoNotMatchProperties(string name, string address, int age) + { + Name = name; + Address = address; + } + } + + // Here, the constructor has three parameters, and two of them match properties + // and one of them match a field. + public class ClassWhereParametersMatchPropertiesAndFields + { + private int Age; + + public string Name { get; } + public string Address { get; } + + public ClassWhereParametersMatchPropertiesAndFields(string name, string address, int age) + { + Name = name; + Address = address; + Age = age; + } + + public int GetAge() => Age; + } + + public record RecordWhereParametersHaveDefaultValue(string Name, string Address, int Age = 42); + + public record ClassWhereParametersHaveDefaultValue + { + public string? Name { get; } + public string Address { get; } + public int Age { get; } + + public ClassWhereParametersHaveDefaultValue(string? name, string address, int age = 42) + { + Name = name; + Address = address; + Age = age; + } + } + + + public record RecordTypeOptions(string Color, int Length); + + public record Line(string Color, int Length, int Thickness); + + public class ClassWithMatchingParametersAndProperties + { + private readonly string _color; + + public ClassWithMatchingParametersAndProperties(string Color, int Length) + { + _color = Color; + this.Length = Length; + } + + public int Length { get; set; } + + public string Color + { + get => _color; + init => _color = "the color is " + value; + } + } + + public readonly record struct ReadonlyRecordStructTypeOptions(string Color, int Length); + + public class ContainerWithNestedImmutableObject + { + public string ContainerName { get; set; } + public ImmutableLengthAndColorClass LengthAndColor { get; set; } + } + + public struct MutableStructWithConstructor + { + public MutableStructWithConstructor(string randomParameter) + { + Color = randomParameter; + Length = randomParameter.Length; + } + + public string Color { get; set; } + public int Length { get; set; } + } + + public class ImmutableLengthAndColorClass + { + public ImmutableLengthAndColorClass(string color, int length) + { + Color = color; + Length = length; + } + + public string Color { get; } + public int Length { get; } + } + + public class ImmutableClassWithOneParameterizedConstructor + { + public ImmutableClassWithOneParameterizedConstructor(string string1, int int1, string string2, int int2) + { + String1 = string1; + Int1 = int1; + String2 = string2; + Int2 = int2; + } + + public string String1 { get; } + public string String2 { get; } + public int Int1 { get; } + public int Int2 { get; } + } + + public class ImmutableClassWithOneParameterizedConstructorButWithInParameter + { + public ImmutableClassWithOneParameterizedConstructorButWithInParameter(in string string1, int int1, string string2, int int2) + { + String1 = string1; + Int1 = int1; + String2 = string2; + Int2 = int2; + } + + public string String1 { get; } + public string String2 { get; } + public int Int1 { get; } + public int Int2 { get; } + } + + public class ImmutableClassWithOneParameterizedConstructorButWithRefParameter + { + public ImmutableClassWithOneParameterizedConstructorButWithRefParameter(string string1, ref int int1, string string2, int int2) + { + String1 = string1; + Int1 = int1; + String2 = string2; + Int2 = int2; + } + + public string String1 { get; } + public string String2 { get; } + public int Int1 { get; } + public int Int2 { get; } + } + + public class ImmutableClassWithOneParameterizedConstructorButWithOutParameter + { + public ImmutableClassWithOneParameterizedConstructorButWithOutParameter(string string1, int int1, + string string2, out decimal int2) + { + String1 = string1; + Int1 = int1; + String2 = string2; + int2 = 0; + } + + public string String1 { get; } + public string String2 { get; } + public int Int1 { get; } + public int Int2 { get; } + } + + public class ImmutableClassWithMultipleParameterizedConstructors + { + public ImmutableClassWithMultipleParameterizedConstructors(string string1, int int1) + { + String1 = string1; + Int1 = int1; + } + + public ImmutableClassWithMultipleParameterizedConstructors(string string1, int int1, string string2) + { + String1 = string1; + Int1 = int1; + String2 = string2; + } + + public ImmutableClassWithMultipleParameterizedConstructors(string string1, int int1, string string2, int int2) + { + String1 = string1; + Int1 = int1; + String2 = string2; + Int2 = int2; + } + + public ImmutableClassWithMultipleParameterizedConstructors(string string1) + { + String1 = string1; + } + + public string String1 { get; } + public string String2 { get; } + public int Int1 { get; } + public int Int2 { get; } + } + + public class SemiImmutableClass + { + public SemiImmutableClass(string color, int length) + { + Color = color; + Length = length; + } + + public string Color { get; } + public int Length { get; } + public decimal Thickness { get; set; } + } + + public class SemiImmutableClassWithInit + { + public SemiImmutableClassWithInit(string color, int length) + { + Color = color; + Length = length; + } + + public string Color { get; } + public int Length { get; } + public decimal Thickness { get; init; } + } + public struct ValueTypeOptions { public int MyInt32 { get; set; } @@ -934,10 +1166,136 @@ public void ExceptionWhenTryingToBindClassWithoutParameterlessConstructor() var exception = Assert.Throws( () => config.Bind(new TestOptions())); Assert.Equal( - SR.Format(SR.Error_MissingParameterlessConstructor, typeof(ClassWithoutPublicConstructor)), + SR.Format(SR.Error_MissingPublicInstanceConstructor, typeof(ClassWithoutPublicConstructor)), + exception.Message); + } + + [Fact] + public void ExceptionWhenTryingToBindClassWherePropertiesDoMatchConstructorParameters() + { + var input = new Dictionary + { + {"ClassWhereParametersDoNotMatchPropertiesProperty:Name", "John"}, + {"ClassWhereParametersDoNotMatchPropertiesProperty:Address", "123, Abc St."}, + {"ClassWhereParametersDoNotMatchPropertiesProperty:Age", "42"} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Bind(new TestOptions())); + Assert.Equal( + SR.Format(SR.Error_ConstructorParametersDoNotMatchProperties, typeof(ClassWhereParametersDoNotMatchProperties), "age"), + exception.Message); + } + + [Fact] + public void ExceptionWhenTryingToBindToConstructorWithMissingConfig() + { + var input = new Dictionary + { + {"LineProperty:Color", "Red"}, + {"LineProperty:Length", "22"} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Bind(new TestOptions())); + Assert.Equal( + SR.Format(SR.Error_ParameterHasNoMatchingConfig, typeof(Line), nameof(Line.Thickness)), + exception.Message); + } + + [Fact] + public void ExceptionWhenTryingToBindConfigToClassWhereNoMatchingParameterIsFoundInConstructor() + { + var input = new Dictionary + { + {"ClassWhereParametersDoNotMatchPropertiesProperty:Name", "John"}, + {"ClassWhereParametersDoNotMatchPropertiesProperty:Address", "123, Abc St."}, + {"ClassWhereParametersDoNotMatchPropertiesProperty:Age", "42"} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Bind(new TestOptions())); + Assert.Equal( + SR.Format(SR.Error_ConstructorParametersDoNotMatchProperties, typeof(ClassWhereParametersDoNotMatchProperties), "age"), + exception.Message); + } + + [Fact] + public void BindsToClassConstructorParametersWithDefaultValues() + { + var input = new Dictionary + { + {"ClassWhereParametersHaveDefaultValueProperty:Name", "John"}, + {"ClassWhereParametersHaveDefaultValueProperty:Address", "123, Abc St."} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + TestOptions testOptions = new TestOptions(); + + config.Bind(testOptions); + Assert.Equal("John", testOptions.ClassWhereParametersHaveDefaultValueProperty.Name); + Assert.Equal("123, Abc St.", testOptions.ClassWhereParametersHaveDefaultValueProperty.Address); + Assert.Equal(42, testOptions.ClassWhereParametersHaveDefaultValueProperty.Age); + } + + [Fact] + public void FieldsNotSupported_ExceptionBindingToConstructorWithParameterMatchingAField() + { + var input = new Dictionary + { + {"ClassWhereParametersMatchPropertiesAndFieldsProperty:Name", "John"}, + {"ClassWhereParametersMatchPropertiesAndFieldsProperty:Address", "123, Abc St."}, + {"ClassWhereParametersMatchPropertiesAndFieldsProperty:Age", "42"} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + var exception = Assert.Throws( + () => config.Bind(new TestOptions())); + + Assert.Equal( + SR.Format(SR.Error_ConstructorParametersDoNotMatchProperties, typeof(ClassWhereParametersMatchPropertiesAndFields), "age"), exception.Message); } + [Fact] + public void BindsToRecordPrimaryConstructorParametersWithDefaultValues() + { + var input = new Dictionary + { + {"RecordWhereParametersHaveDefaultValueProperty:Name", "John"}, + {"RecordWhereParametersHaveDefaultValueProperty:Address", "123, Abc St."} + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(input); + var config = configurationBuilder.Build(); + + TestOptions testOptions = new TestOptions(); + + config.Bind(testOptions); + Assert.Equal("John", testOptions.RecordWhereParametersHaveDefaultValueProperty.Name); + Assert.Equal("123, Abc St.", testOptions.RecordWhereParametersHaveDefaultValueProperty.Address); + Assert.Equal(42, testOptions.RecordWhereParametersHaveDefaultValueProperty.Age); + } + [Fact] public void ExceptionWhenTryingToBindToTypeThrowsWhenActivated() { @@ -994,6 +1352,280 @@ public void CanBindValueTypeOptions() Assert.Equal("hello world", options.MyString); } + [Fact] + public void CanBindImmutableClass() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.Color); + } + + [Fact] + public void CanBindMutableClassWitNestedImmutableObject() + { + var dic = new Dictionary + { + {"ContainerName", "Container123"}, + {"LengthAndColor:Length", "42"}, + {"LengthAndColor:Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal("Container123", options.ContainerName); + Assert.Equal(42, options.LengthAndColor.Length); + Assert.Equal("Green", options.LengthAndColor.Color); + } + + // If the immutable type has multiple public parameterized constructors, then throw + // an exception. + [Fact] + public void CanBindImmutableClass_ThrowsOnMultipleParameterizedConstructors() + { + var dic = new Dictionary + { + {"String1", "s1"}, + {"Int1", "1"}, + {"String2", "s2"}, + {"Int2", "2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + string expectedMessage = SR.Format(SR.Error_MultipleParameterizedConstructors, "Microsoft.Extensions.Configuration.Binder.Test.ConfigurationBinderTests+ImmutableClassWithMultipleParameterizedConstructors"); + + var ex = Assert.Throws(() => config.Get()); + + Assert.Equal(expectedMessage, ex.Message); + } + + // If the immutable type has a parameterized constructor, then throw + // that constructor has an 'in' parameter + [Fact] + public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnInParameter() + { + var dic = new Dictionary + { + {"String1", "s1"}, + {"Int1", "1"}, + {"String2", "s2"}, + {"Int2", "2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + string expectedMessage = SR.Format(SR.Error_CannotBindToConstructorParameter, "Microsoft.Extensions.Configuration.Binder.Test.ConfigurationBinderTests+ImmutableClassWithOneParameterizedConstructorButWithInParameter", "string1"); + + var ex = Assert.Throws(() => config.Get()); + + Assert.Equal(expectedMessage, ex.Message); + } + + // If the immutable type has a parameterized constructors, then throw + // that constructor has a 'ref' parameter + [Fact] + public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithARefParameter() + { + var dic = new Dictionary + { + {"String1", "s1"}, + {"Int1", "1"}, + {"String2", "s2"}, + {"Int2", "2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + string expectedMessage = SR.Format(SR.Error_CannotBindToConstructorParameter, "Microsoft.Extensions.Configuration.Binder.Test.ConfigurationBinderTests+ImmutableClassWithOneParameterizedConstructorButWithRefParameter", "int1"); + + var ex = Assert.Throws(() => config.Get()); + + Assert.Equal(expectedMessage, ex.Message); + } + + // If the immutable type has a parameterized constructors, then throw + // if the constructor has an 'out' parameter + [Fact] + public void CanBindImmutableClass_ThrowsOnParameterizedConstructorWithAnOutParameter() + { + var dic = new Dictionary + { + {"String1", "s1"}, + {"Int1", "1"}, + {"String2", "s2"}, + {"Int2", "2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + string expectedMessage = SR.Format(SR.Error_CannotBindToConstructorParameter, "Microsoft.Extensions.Configuration.Binder.Test.ConfigurationBinderTests+ImmutableClassWithOneParameterizedConstructorButWithOutParameter", "int2"); + + var ex = Assert.Throws(() => config.Get()); + + Assert.Equal(expectedMessage, ex.Message); + } + + [Fact] + public void CanBindMutableStruct_UnmatchedConstructorsAreIgnored() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.Color); + } + + // If the immutable type has a public parameterized constructor, + // then pick it. + [Fact] + public void CanBindImmutableClass_PicksParameterizedConstructorIfNoParameterlessConstructorExists() + { + var dic = new Dictionary + { + {"String1", "s1"}, + {"Int1", "1"}, + {"String2", "s2"}, + {"Int2", "2"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal("s1", options.String1); + Assert.Equal("s2", options.String2); + Assert.Equal(1, options.Int1); + Assert.Equal(2, options.Int2); + } + + [Fact] + public void CanBindSemiImmutableClass() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + {"Thickness", "1.23"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.Color); + Assert.Equal(1.23m, options.Thickness); + } + + [Fact] + public void CanBindSemiImmutableClass_WithInitProperties() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + {"Thickness", "1.23"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.Color); + Assert.Equal(1.23m, options.Thickness); + } + + [Fact] + public void CanBindRecordOptions() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.Color); + } + + [Fact] + public void CanBindRecordStructOptions() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.Color); + } + + [Fact] + public void CanBindOnParametersAndProperties_PropertiesAreSetAfterTheConstructor() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("the color is Green", options.Color); + } + + [Fact] + public void CanBindReadonlyRecordStructOptions() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.Color); + } + [Fact] public void CanBindByteArray() { @@ -1153,6 +1785,11 @@ private class TestOptions public ISomeInterface ISomeInterfaceProperty { get; set; } public ClassWithoutPublicConstructor ClassWithoutPublicConstructorProperty { get; set; } + public ClassWhereParametersDoNotMatchProperties ClassWhereParametersDoNotMatchPropertiesProperty { get; set; } + public Line LineProperty { get; set; } + public ClassWhereParametersHaveDefaultValue ClassWhereParametersHaveDefaultValueProperty { get; set; } + public ClassWhereParametersMatchPropertiesAndFields ClassWhereParametersMatchPropertiesAndFieldsProperty { get; set; } + public RecordWhereParametersHaveDefaultValue RecordWhereParametersHaveDefaultValueProperty { get; set; } public int IntProperty { get; set; } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Microsoft.Extensions.Configuration.Binder.Tests.csproj b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Microsoft.Extensions.Configuration.Binder.Tests.csproj index 8fcc6f146609b..c15ba1c1c69d1 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Microsoft.Extensions.Configuration.Binder.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Microsoft.Extensions.Configuration.Binder.Tests.csproj @@ -10,6 +10,7 @@ Link="Common\ConfigurationProviderExtensions.cs" /> +