diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index bc7b74e24db8be..c516bb7520cfaf 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -204,7 +204,7 @@ public static void Bind(this IConfiguration configuration, object? instance, Act [RequiresUnreferencedCode(PropertyTrimmingWarningMessage)] private static void BindNonScalar(this IConfiguration configuration, object instance, BinderOptions options) { - PropertyInfo[] modelProperties = GetAllProperties(instance.GetType()); + List modelProperties = GetAllProperties(instance.GetType()); if (options.ErrorOnUnknownConfiguration) { @@ -451,7 +451,7 @@ private static object CreateInstance( } - PropertyInfo[] properties = GetAllProperties(type); + List properties = GetAllProperties(type); if (!DoAllParametersHaveEquivalentProperties(parameters, properties, out string nameOfInvalidParameters)) { @@ -482,7 +482,7 @@ private static object CreateInstance( } private static bool DoAllParametersHaveEquivalentProperties(ParameterInfo[] parameters, - PropertyInfo[] properties, out string missing) + List properties, out string missing) { HashSet propertyNames = new(StringComparer.OrdinalIgnoreCase); foreach (PropertyInfo prop in properties) @@ -752,8 +752,32 @@ private static bool IsArrayCompatibleReadOnlyInterface(Type type) return null; } - private static PropertyInfo[] GetAllProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type) - => type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); + private static List GetAllProperties([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type) + { + var allProperties = new List(); + + Type baseType = type; + while (baseType != typeof(object)) + { + PropertyInfo[] properties = baseType.GetProperties(DeclaredOnlyLookup); + + foreach (PropertyInfo property in properties) + { + // if the property is virtual, only add the base-most definition so + // overriden properties aren't duplicated in the list. + MethodInfo? setMethod = property.GetSetMethod(true); + + if (setMethod is null || !setMethod.IsVirtual || setMethod == setMethod.GetBaseDefinition()) + { + allProperties.Add(property); + } + } + + baseType = baseType.BaseType!; + } + + return allProperties; + } [RequiresUnreferencedCode(PropertyTrimmingWarningMessage)] private static object? BindParameter(ParameterInfo parameter, Type type, IConfiguration config, diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 16d58c6166cec8..afe17336d20470 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -1751,20 +1751,45 @@ public void CanBindNullableNestedStructProperties() } [Fact] - public void CanBindVirtualPropertiesWithoutDuplicates() + public void CanBindVirtualProperties() { ConfigurationBuilder configurationBuilder = new(); configurationBuilder.AddInMemoryCollection(new Dictionary { - { "Test:0", "1" } + { $"{nameof(BaseClassWithVirtualProperty.Test)}:0", "1" }, + { $"{nameof(BaseClassWithVirtualProperty.TestGetSetOverriden)}", "2" }, + { $"{nameof(BaseClassWithVirtualProperty.TestGetOverriden)}", "3" }, + { $"{nameof(BaseClassWithVirtualProperty.TestSetOverriden)}", "4" }, + { $"{nameof(BaseClassWithVirtualProperty.TestNoOverriden)}", "5" }, + { $"{nameof(BaseClassWithVirtualProperty.TestVirtualSet)}", "6" } }); IConfiguration config = configurationBuilder.Build(); var test = new ClassOverridingVirtualProperty(); config.Bind(test); + Assert.Equal("1", Assert.Single(test.Test)); + Assert.Equal("2", test.TestGetSetOverriden); + Assert.Equal("3", test.TestGetOverriden); + Assert.Equal("4", test.TestSetOverriden); + Assert.Equal("5", test.TestNoOverriden); + Assert.Null(test.ExposeTestVirtualSet()); } + [Fact] + public void CanBindPrivatePropertiesFromBaseClass() + { + ConfigurationBuilder configurationBuilder = new(); + configurationBuilder.AddInMemoryCollection(new Dictionary + { + { "PrivateProperty", "a" } + }); + IConfiguration config = configurationBuilder.Build(); + + var test = new ClassOverridingVirtualProperty(); + config.Bind(test, b => b.BindNonPublicProperties = true); + Assert.Equal("a", test.ExposePrivatePropertyValue()); + } private interface ISomeInterface { @@ -1845,12 +1870,43 @@ public struct DeeplyNested public class BaseClassWithVirtualProperty { + private string? PrivateProperty { get; set; } + public virtual string[] Test { get; set; } = System.Array.Empty(); + + public virtual string? TestGetSetOverriden { get; set; } + public virtual string? TestGetOverriden { get; set; } + public virtual string? TestSetOverriden { get; set; } + + private string? _testVirtualSet; + public virtual string? TestVirtualSet + { + set => _testVirtualSet = value; + } + + public virtual string? TestNoOverriden { get; set; } + + public string? ExposePrivatePropertyValue() => PrivateProperty; } public class ClassOverridingVirtualProperty : BaseClassWithVirtualProperty { public override string[] Test { get => base.Test; set => base.Test = value; } + + public override string? TestGetSetOverriden { get; set; } + public override string? TestGetOverriden => base.TestGetOverriden; + public override string? TestSetOverriden + { + set => base.TestSetOverriden = value; + } + + private string? _testVirtualSet; + public override string? TestVirtualSet + { + set => _testVirtualSet = value; + } + + public string? ExposeTestVirtualSet() => _testVirtualSet; } } }