Skip to content

Commit

Permalink
Support immutable types with configuration binding (#67258)
Browse files Browse the repository at this point in the history
Merging. Thanks @SteveDunn for your contribution.
  • Loading branch information
SteveDunn committed Jun 9, 2022
1 parent 77a8d3c commit d78f364
Show file tree
Hide file tree
Showing 5 changed files with 790 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.Internal;

namespace Microsoft.Extensions.Configuration
{
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -407,23 +409,63 @@ private static object BindToCollection(Type type, IConfiguration config, BinderO
}
}

[RequiresUnreferencedCode("In case type is a Nullable<T>, 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<T>, 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<PropertyInfo> 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;
Expand All @@ -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<PropertyInfo> properties, out string missing)
{
HashSet<string> propertyNames = new(StringComparer.OrdinalIgnoreCase);
foreach (PropertyInfo prop in properties)
{
propertyNames.Add(prop.Name);
}

List<string> 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,
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@

<ItemGroup>
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Extensions.Configuration.Abstractions\src\Microsoft.Extensions.Configuration.Abstractions.csproj" />
<Compile Include="$(CommonPath)System\ThrowHelper.cs"
Link="Common\System\ThrowHelper.cs" />
<Compile Include="$(CommonPath)Extensions\ParameterDefaultValue\ParameterDefaultValue.cs" Link="Common\src\Extensions\ParameterDefaultValue\ParameterDefaultValue.cs" />
<Compile Include="$(CommonPath)System\ThrowHelper.cs" Link="Common\System\ThrowHelper.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' != '.NETCoreApp'">
<Compile Include="$(CommonPath)Extensions\ParameterDefaultValue\ParameterDefaultValue.netstandard.cs" Link="Common\src\Extensions\ParameterDefaultValue\ParameterDefaultValue.netstandard.cs" />
<Compile Include="$(CoreLibSharedDir)System\Diagnostics\CodeAnalysis\DynamicallyAccessedMembersAttribute.cs" />
<Compile Include="$(CoreLibSharedDir)System\Diagnostics\CodeAnalysis\DynamicallyAccessedMemberTypes.cs" />
<Compile Include="$(CoreLibSharedDir)System\Diagnostics\CodeAnalysis\RequiresUnreferencedCodeAttribute.cs" />
<Compile Include="$(CoreLibSharedDir)System\Diagnostics\CodeAnalysis\UnconditionalSuppressMessageAttribute.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFrameworkIdentifier)' == '.NETCoreApp'">
<Compile Include="$(CommonPath)Extensions\ParameterDefaultValue\ParameterDefaultValue.netcoreapp.cs" Link="Common\src\Extensions\ParameterDefaultValue\ParameterDefaultValue.netcoreapp.cs" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@
<data name="Error_CannotActivateAbstractOrInterface" xml:space="preserve">
<value>Cannot create instance of type '{0}' because it is either abstract or an interface.</value>
</data>
<data name="Error_CannotBindToConstructorParameter" xml:space="preserve">
<value>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}'</value>
</data>
<data name="Error_ConstructorParametersDoNotMatchProperties" xml:space="preserve">
<value>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}'</value>
</data>
<data name="Error_FailedBinding" xml:space="preserve">
<value>Failed to convert configuration value at '{0}' to type '{1}'.</value>
</data>
Expand All @@ -129,8 +135,17 @@
<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 name="Error_MissingPublicInstanceConstructor" xml:space="preserve">
<value>Cannot create instance of type '{0}' because it is missing a public instance constructor.</value>
</data>
<data name="Error_MultipleParameterizedConstructors" xml:space="preserve">
<value>Cannot create instance of type '{0}' because it has multiple public parameterized constructors.</value>
</data>
<data name="Error_ParameterBeingBoundToIsUnnamed" xml:space="preserve">
<value>Cannot create instance of type '{0}' because one or more parameters are unnamed.</value>
</data>
<data name="Error_ParameterHasNoMatchingConfig" xml:space="preserve">
<value>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.</value>
</data>
<data name="Error_UnsupportedMultidimensionalArray" xml:space="preserve">
<value>Cannot create instance of type '{0}' because multidimensional arrays are not supported.</value>
Expand Down
Loading

0 comments on commit d78f364

Please sign in to comment.