diff --git a/src/libraries/Microsoft.Extensions.Configuration.Abstractions/ref/Microsoft.Extensions.Configuration.Abstractions.cs b/src/libraries/Microsoft.Extensions.Configuration.Abstractions/ref/Microsoft.Extensions.Configuration.Abstractions.cs index c743a37346c22f..6c83ffe225776b 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Abstractions/ref/Microsoft.Extensions.Configuration.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Abstractions/ref/Microsoft.Extensions.Configuration.Abstractions.cs @@ -15,6 +15,12 @@ public static partial class ConfigurationExtensions public static string GetConnectionString(this Microsoft.Extensions.Configuration.IConfiguration configuration, string name) { throw null; } public static Microsoft.Extensions.Configuration.IConfigurationSection GetRequiredSection(this Microsoft.Extensions.Configuration.IConfiguration configuration, string key) { throw null; } } + [System.AttributeUsageAttribute(System.AttributeTargets.Property)] + public sealed partial class ConfigurationKeyNameAttribute : System.Attribute + { + public ConfigurationKeyNameAttribute(string name) { } + public string Name { get { throw null; } } + } public static partial class ConfigurationPath { public static readonly string KeyDelimiter; diff --git a/src/libraries/Microsoft.Extensions.Configuration.Abstractions/src/ConfigurationKeyNameAttribute.cs b/src/libraries/Microsoft.Extensions.Configuration.Abstractions/src/ConfigurationKeyNameAttribute.cs new file mode 100644 index 00000000000000..d9aeb155804f1c --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Abstractions/src/ConfigurationKeyNameAttribute.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace Microsoft.Extensions.Configuration +{ + [AttributeUsage(AttributeTargets.Property)] + public sealed class ConfigurationKeyNameAttribute : Attribute + { + public ConfigurationKeyNameAttribute(string name) => Name = name; + + public string Name { get; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs index 4d2e5cf4889a9a..33aa11eaed985d 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/src/ConfigurationBinder.cs @@ -208,7 +208,7 @@ private static void BindProperty(PropertyInfo property, object instance, IConfig return; } - propertyValue = BindInstance(property.PropertyType, propertyValue, config.GetSection(property.Name), options); + propertyValue = GetPropertyValue(property, instance, config, options); if (propertyValue != null && hasSetter) { @@ -575,5 +575,48 @@ private static IEnumerable GetAllProperties(Type type) return allProperties; } + + private static object GetPropertyValue(PropertyInfo property, object instance, IConfiguration config, BinderOptions options) + { + string propertyName = GetPropertyName(property); + return BindInstance( + property.PropertyType, + property.GetValue(instance), + config.GetSection(propertyName), + options); + } + + private static string GetPropertyName(MemberInfo property) + { + if (property == null) + { + throw new ArgumentNullException(nameof(property)); + } + + // Check for a custom property name used for configuration key binding + foreach (var attributeData in property.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 : property.Name; + } + + return property.Name; + } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs index 2195e56c596d8a..734bcf3ed4aca5 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/ConfigurationBinderTests.cs @@ -34,6 +34,9 @@ public ComplexOptions() internal string InternalProperty { get; set; } protected string ProtectedProperty { get; set; } + [ConfigurationKeyName("Named_Property")] + public string NamedProperty { get; set; } + protected string ProtectedPrivateSet { get; private set; } private string PrivateReadOnly { get; } @@ -201,6 +204,22 @@ public void CanBindIConfigurationSectionWithDerivedOptionsSection() Assert.Null(options.Section.Value); } + [Fact] + public void CanBindConfigurationKeyNameAttributes() + { + var dic = new Dictionary + { + {"Named_Property", "Yo"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + + Assert.Equal("Yo", options.NamedProperty); + } + [Fact] public void EmptyStringIsNullable() {