Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
{
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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)
{
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the plan for addressing this?

I agree that this code doesn't make sense because these attributes can't be on parameters:

[AttributeUsage(AttributeTargets.Property)]
public sealed class ConfigurationKeyNameAttribute : Attribute

// not parameters. We might be able to get rid of this method once confirm whether it's needed or not.

private static string GetParameterName(ParameterInfo parameter)
{
// Check for a custom property name used for configuration key binding
foreach (var attributeData in parameter.GetCustomAttributesData())
{
if (attributeData.AttributeType != typeof(ConfigurationKeyNameAttribute))
{
continue;
}

// Ensure ConfigurationKeyName constructor signature matches expectations
if (attributeData.ConstructorArguments.Count != 1)
{
break;
}

// Assumes ConfigurationKeyName constructor first arg is the string key name
string? name = attributeData
.ConstructorArguments[0]
.Value?
.ToString();

return !string.IsNullOrWhiteSpace(name) ? name : parameter.Name!;
}

return parameter.Name!;
}

private static string GetPropertyName(MemberInfo property!!)
{
// Check for a custom property name used for configuration key binding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,75 @@ public class DerivedOptionsWithIConfigurationSection : DerivedOptions
public IConfigurationSection DerivedSection { get; set; }
}

#if NETCOREAPP
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this only on NETCOREAPP?

public record RecordTypeOptions(string Color, int Length);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about tests for readonly struct and record struct?

#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; }
Expand Down Expand Up @@ -994,6 +1063,107 @@ public void CanBindValueTypeOptions()
Assert.Equal("hello world", options.MyString);
}

[Fact]
public void CanBindImmutableClass()
{
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()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFrameworks>$(NetCoreAppCurrent);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum)</TargetFrameworks>
<IsPackable>true</IsPackable>
<NoWarn>$(NoWarn);CA1850</NoWarn> <!-- CA1850 suppressed due to multitargeting -->
<Nullable>enable</Nullable>
<PackageDescription>Provides classes to support the creation and validation of XML digital signatures. The classes in this namespace implement the World Wide Web Consortium Recommendation, "XML-Signature Syntax and Processing", described at http://www.w3.org/TR/xmldsig-core/.

Commonly Used Types:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ internal NamespaceFrame GetCurrentScope()
return GetScopeAt(_ancestorStack.Count - 1);
}

protected XmlAttribute GetNearestRenderedNamespaceWithMatchingPrefix(string nsPrefix, out int depth)
protected XmlAttribute? GetNearestRenderedNamespaceWithMatchingPrefix(string nsPrefix, out int depth)
{
XmlAttribute attr;
XmlAttribute? attr;
depth = -1;
for (int i = _ancestorStack.Count - 1; i >= 0; i--)
{
Expand All @@ -35,9 +35,9 @@ protected XmlAttribute GetNearestRenderedNamespaceWithMatchingPrefix(string nsPr
return null;
}

protected XmlAttribute GetNearestUnrenderedNamespaceWithMatchingPrefix(string nsPrefix, out int depth)
protected XmlAttribute? GetNearestUnrenderedNamespaceWithMatchingPrefix(string nsPrefix, out int depth)
{
XmlAttribute attr;
XmlAttribute? attr;
depth = -1;
for (int i = _ancestorStack.Count - 1; i >= 0; i--)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ internal sealed class AttributeSortOrder : IComparer
{
internal AttributeSortOrder() { }

public int Compare(object a, object b)
public int Compare(object? a, object? b)
{
XmlNode nodeA = a as XmlNode;
XmlNode nodeB = b as XmlNode;
XmlNode? nodeA = a as XmlNode;
XmlNode? nodeB = b as XmlNode;
if ((nodeA == null) || (nodeB == null))
throw new ArgumentException();
int namespaceCompare = string.CompareOrdinal(nodeA.NamespaceURI, nodeB.NamespaceURI);
Expand Down
Loading