-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Support binding to immutable types #43662 #67161
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9bf910f
9d70502
b3c423b
074dfb7
961dfb5
47b199f
e3fcbd0
7e35675
6bb530b
6150dbd
d7356e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,10 +2,10 @@ | |||||
| // The .NET Foundation licenses this file to you under the MIT license. | ||||||
|
|
||||||
| using System; | ||||||
| using System.Buffers; | ||||||
| using System.Collections; | ||||||
| using System.Collections.Generic; | ||||||
| using System.ComponentModel; | ||||||
| using System.Diagnostics; | ||||||
| using System.Diagnostics.CodeAnalysis; | ||||||
| using System.Linq; | ||||||
| using System.Reflection; | ||||||
|
|
@@ -359,7 +359,7 @@ private static void BindProperty(PropertyInfo property, object instance, IConfig | |||||
| return instance; | ||||||
| } | ||||||
|
|
||||||
| instance = CreateInstance(type); | ||||||
| instance = CreateInstance(type, config, options); | ||||||
| } | ||||||
|
|
||||||
| // See if its a Dictionary | ||||||
|
|
@@ -400,7 +400,12 @@ private static void BindProperty(PropertyInfo property, object instance, IConfig | |||||
| return instance; | ||||||
| } | ||||||
|
|
||||||
| private static object? CreateInstance([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type) | ||||||
| [RequiresUnreferencedCode(PropertyTrimmingWarningMessage)] | ||||||
| private static object? CreateInstance( | ||||||
| [DynamicallyAccessedMembers( | ||||||
| DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type, | ||||||
| IConfiguration config, | ||||||
| BinderOptions options) | ||||||
| { | ||||||
| if (type.IsInterface || type.IsAbstract) | ||||||
| { | ||||||
|
|
@@ -419,10 +424,41 @@ private static void BindProperty(PropertyInfo property, object instance, IConfig | |||||
|
|
||||||
| if (!type.IsValueType) | ||||||
| { | ||||||
| bool hasDefaultConstructor = type.GetConstructors(DeclaredOnlyLookup).Any(ctor => ctor.IsPublic && ctor.GetParameters().Length == 0); | ||||||
| if (!hasDefaultConstructor) | ||||||
| ConstructorInfo[] constructors = type.GetConstructors(DeclaredOnlyLookup); | ||||||
|
|
||||||
| bool hasParameterlessPublicConstructor = constructors.Any(ctor => ctor.IsPublic && ctor.GetParameters().Length == 0); | ||||||
|
|
||||||
| if (!hasParameterlessPublicConstructor) | ||||||
| { | ||||||
| throw new InvalidOperationException(SR.Format(SR.Error_MissingParameterlessConstructor, type)); | ||||||
| // if the only constructor is private, then throw | ||||||
| if (constructors.Length == 1 && !constructors[0].IsPublic) | ||||||
| { | ||||||
| throw new InvalidOperationException(SR.Format(SR.Error_MissingParameterlessConstructor, type)); | ||||||
| } | ||||||
|
|
||||||
| // find the biggest constructor so that we can bind to the most parameters | ||||||
| ParameterInfo[] parameters = constructors[0].GetParameters(); | ||||||
|
|
||||||
| int indexOfChosenConstructor = 0; | ||||||
|
|
||||||
| for (int index = 1; index < constructors.Length; index++) | ||||||
| { | ||||||
| ParameterInfo[] constructorParameters = constructors[index].GetParameters(); | ||||||
| if (constructorParameters.Length > parameters.Length) | ||||||
| { | ||||||
| parameters = constructorParameters; | ||||||
| indexOfChosenConstructor = index; | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| object?[] parameterValues = new object?[parameters.Length]; | ||||||
|
|
||||||
| for (int index = 0; index < parameters.Length; index++) | ||||||
| { | ||||||
| parameterValues[index] = GetParameterValue(parameters[index], config, options); | ||||||
| } | ||||||
|
|
||||||
| return constructors[indexOfChosenConstructor].Invoke(parameterValues); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
@@ -687,6 +723,18 @@ private static List<PropertyInfo> GetAllProperties( | |||||
| return allProperties; | ||||||
| } | ||||||
|
|
||||||
| [RequiresUnreferencedCode(PropertyTrimmingWarningMessage)] | ||||||
| private static object? GetParameterValue(ParameterInfo property, IConfiguration config, BinderOptions options) | ||||||
| { | ||||||
| string parameterName = GetParameterName(property); | ||||||
|
|
||||||
| return BindInstance( | ||||||
| property.ParameterType, | ||||||
| null, | ||||||
| config.GetSection(parameterName), | ||||||
| options); | ||||||
| } | ||||||
|
|
||||||
| [RequiresUnreferencedCode(PropertyTrimmingWarningMessage)] | ||||||
| private static object? GetPropertyValue(PropertyInfo property, object instance, IConfiguration config, BinderOptions options) | ||||||
| { | ||||||
|
|
@@ -698,6 +746,37 @@ private static List<PropertyInfo> GetAllProperties( | |||||
| options); | ||||||
| } | ||||||
|
|
||||||
| // todo: steve - we might not need this; currently, these attributes are only applicable to properties and | ||||||
|
||||||
| [AttributeUsage(AttributeTargets.Property)] | |
| public sealed class ConfigurationKeyNameAttribute : Attribute |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -112,6 +112,75 @@ public class DerivedOptionsWithIConfigurationSection : DerivedOptions | |
| public IConfigurationSection DerivedSection { get; set; } | ||
| } | ||
|
|
||
| #if NETCOREAPP | ||
|
||
| public record RecordTypeOptions(string Color, int Length); | ||
|
||
| #endif | ||
|
|
||
| public class ContainerWithNestedImmutableObject | ||
| { | ||
| public string ContainerName { get; set; } | ||
| public ImmutableLengthAndColorClass LengthAndColor { 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 ImmutableClassWithConstructorOverloads | ||
| { | ||
| public ImmutableClassWithConstructorOverloads(string string1, int int1) | ||
| { | ||
| String1 = string1; | ||
| Int1 = int1; | ||
| } | ||
|
|
||
| public ImmutableClassWithConstructorOverloads(string string1, int int1, string string2) | ||
| { | ||
| String1 = string1; | ||
| Int1 = int1; | ||
| String2 = string2; | ||
| } | ||
|
|
||
| public ImmutableClassWithConstructorOverloads(string string1, int int1, string string2, int int2) | ||
| { | ||
| String1 = string1; | ||
| Int1 = int1; | ||
| String2 = string2; | ||
| Int2 = int2; | ||
| } | ||
|
|
||
| public ImmutableClassWithConstructorOverloads(string string1) | ||
| { | ||
| String1 = string1; | ||
| } | ||
|
|
||
| public string String1 { get; } | ||
| public string String2 { get; } | ||
| public int Int1 { get; } | ||
| public int Int2 { get; } | ||
| } | ||
|
|
||
| public class SemiImmutableType | ||
| { | ||
| public SemiImmutableType(string color, int length) | ||
| { | ||
| Color = color; | ||
| Length = length; | ||
| } | ||
|
|
||
| public string Color { get; } | ||
| public int Length { get; } | ||
| public decimal Thickness { get; set; } | ||
| } | ||
|
|
||
| public struct ValueTypeOptions | ||
| { | ||
| public int MyInt32 { get; set; } | ||
|
|
@@ -994,6 +1063,107 @@ public void CanBindValueTypeOptions() | |
| Assert.Equal("hello world", options.MyString); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void CanBindImmutableClass() | ||
SteveDunn marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| { | ||
| var dic = new Dictionary<string, string> | ||
| { | ||
| {"Length", "42"}, | ||
| {"Color", "Green"}, | ||
| }; | ||
| var configurationBuilder = new ConfigurationBuilder(); | ||
| configurationBuilder.AddInMemoryCollection(dic); | ||
| var config = configurationBuilder.Build(); | ||
|
|
||
| var options = config.Get<ImmutableLengthAndColorClass>(); | ||
| Assert.Equal(42, options.Length); | ||
| Assert.Equal("Green", options.Color); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void CanBindMutableClassWitNestedImmutableObject() | ||
| { | ||
| var dic = new Dictionary<string, string> | ||
| { | ||
| {"ContainerName", "Container123"}, | ||
| {"LengthAndColor:Length", "42"}, | ||
| {"LengthAndColor:Color", "Green"}, | ||
| }; | ||
| var configurationBuilder = new ConfigurationBuilder(); | ||
| configurationBuilder.AddInMemoryCollection(dic); | ||
| var config = configurationBuilder.Build(); | ||
|
|
||
| var options = config.Get<ContainerWithNestedImmutableObject>(); | ||
| Assert.Equal("Container123", options.ContainerName); | ||
| Assert.Equal(42, options.LengthAndColor.Length); | ||
| Assert.Equal("Green", options.LengthAndColor.Color); | ||
| } | ||
|
|
||
| // If the immutable type has multiple constructors, | ||
| // then pick the one with the most parameters | ||
| // and try to bind as many as possible. | ||
| // The type used in example below has multiple constructors in | ||
| // an arbitrary order, but/ with the biggest (chosen) one | ||
| // deliberately not first or last. | ||
| [Fact] | ||
| public void CanBindImmutableClass_PicksBiggestNonParameterlessConstructor() | ||
| { | ||
| var dic = new Dictionary<string, string> | ||
| { | ||
| {"String1", "s1"}, | ||
| {"Int1", "1"}, | ||
| {"String2", "s2"}, | ||
| {"Int2", "2"}, | ||
| }; | ||
| var configurationBuilder = new ConfigurationBuilder(); | ||
| configurationBuilder.AddInMemoryCollection(dic); | ||
| var config = configurationBuilder.Build(); | ||
|
|
||
| var options = config.Get<ImmutableClassWithConstructorOverloads>(); | ||
| 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<string, string> | ||
| { | ||
| {"Length", "42"}, | ||
| {"Color", "Green"}, | ||
| {"Thickness", "1.23"}, | ||
| }; | ||
| var configurationBuilder = new ConfigurationBuilder(); | ||
| configurationBuilder.AddInMemoryCollection(dic); | ||
| var config = configurationBuilder.Build(); | ||
|
|
||
| var options = config.Get<SemiImmutableType>(); | ||
| Assert.Equal(42, options.Length); | ||
| Assert.Equal("Green", options.Color); | ||
| Assert.Equal(1.23m, options.Thickness); | ||
| } | ||
|
|
||
| #if NETCOREAPP | ||
| [Fact] | ||
| public void CanBindRecordOptions() | ||
| { | ||
| var dic = new Dictionary<string, string> | ||
| { | ||
| {"Length", "42"}, | ||
| {"Color", "Green"}, | ||
| }; | ||
| var configurationBuilder = new ConfigurationBuilder(); | ||
| configurationBuilder.AddInMemoryCollection(dic); | ||
| var config = configurationBuilder.Build(); | ||
|
|
||
| var options = config.Get<RecordTypeOptions>(); | ||
| Assert.Equal(42, options.Length); | ||
| Assert.Equal("Green", options.Color); | ||
| } | ||
| #endif | ||
|
|
||
| [Fact] | ||
| public void CanBindByteArray() | ||
| { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.