diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs index 42979bf896c101..e89bad8ded56f7 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs @@ -359,8 +359,10 @@ private void EmitInitializeMethod(ObjectSpec type) EmitStartBlock(returnExpression); foreach (PropertySpec property in initOnlyProps) { - string propertyName = property.Name; - _writer.WriteLine($@"{propertyName} = {propertyName},"); + // Properties bound through a matching constructor parameter don't have a local of their + // own; their bound value lives in the local named after the parameter. + string valueExpr = property.MatchingCtorParam?.Name ?? property.Name; + _writer.WriteLine($@"{property.Name} = {valueExpr},"); } EmitEndBlock(endBraceTrailingSource: ";"); } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs index 184eb5fa6cf297..bd2353717596f0 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.cs @@ -196,6 +196,27 @@ public string Color } } + public class ClassWithMatchingParametersAndProperties_DifferentlyCasedCtorParam + { + private readonly string _color; + + public ClassWithMatchingParametersAndProperties_DifferentlyCasedCtorParam(string color, int length) + { + _color = color; + this.ColorFromCtor = color; + this.Length = length; + } + + public int Length { get; set; } + + public string ColorFromCtor { get; } + public string Color + { + get => _color; + init => _color = "the color is " + value; + } + } + public sealed class TreeElement : Dictionary; public record TypeWithRecursionThroughCollections diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs index 0197d107afc384..034bea66dfb1bd 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.cs @@ -1646,6 +1646,24 @@ public void CanBindOnParametersAndProperties_PropertiesAreSetAfterTheConstructor Assert.Equal("the color is Green", options.Color); } + [Fact] + public void CanBindOnParametersAndProperties_DifferentlyCasedConstructorParameter() + { + var dic = new Dictionary + { + {"Length", "42"}, + {"Color", "Green"}, + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + var config = configurationBuilder.Build(); + + var options = config.Get(); + Assert.Equal(42, options.Length); + Assert.Equal("Green", options.ColorFromCtor); + Assert.Equal("the color is Green", options.Color); + } + /// /// This test to ensure the binding of the constructor/property array is done once and not duplicating values in the array. /// diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs index 9dd14fd5dc2886..5578c0c3fde3fc 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.Helpers.cs @@ -206,17 +206,15 @@ private static HashSet GetFilteredAssemblyRefs(IEnumerable exclu return assemblies; } - public static byte[] CreateAssemblyImage(Compilation compilation) + private static void AssertCanCreateAssemblyImage(Compilation compilation) { - MemoryStream ms = new MemoryStream(); - var emitResult = compilation.Emit(ms); + var emitResult = compilation.Emit(Stream.Null); if (!emitResult.Success) { // Explicit failures to include in the test output. string errorMessage = string.Join(Environment.NewLine, emitResult.Diagnostics.Select(d => d.ToString())); throw new InvalidOperationException(errorMessage); } - return ms.ToArray(); } } } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs index d74ac253e616eb..47e3c33a15826f 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs @@ -290,10 +290,63 @@ public class Settings Assert.NotNull(result.GeneratedSource); Assert.Empty(result.Diagnostics); - // Ensure the generated code can be compiled. - // If there is any compilation error, exception will be thrown with the list of the errors in the exception message. - byte[] emittedAssemblyImage = CreateAssemblyImage(result.OutputCompilation); - Assert.NotNull(emittedAssemblyImage); + AssertCanCreateAssemblyImage(result.OutputCompilation); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNetCore))] + // Required, settable property whose value flows through a constructor marked [SetsRequiredMembers] + // whose parameter name differs only in casing from the property. + [InlineData(""" + [method: SetsRequiredMembers] + public class GreetSettings(string name) + { + public string Greeting { get; set; } = "Hello"; + public required string Name { get; set; } = name; + } + """)] + // Same as above, but the constructor parameter name matches the property name exactly. + [InlineData(""" + [method: SetsRequiredMembers] + public class GreetSettings(string Name) + { + public string Greeting { get; set; } = "Hello"; + public required string Name { get; set; } = Name; + } + """)] + // Required property set via a constructor parameter, but the constructor does not set required + // members, so the property must still be assigned through the object initializer. + [InlineData(""" + public class GreetSettings(string name) + { + public string Greeting { get; set; } = "Hello"; + public required string Name { get; set; } = name; + } + """)] + public async Task RequiredPropertyWithMatchingConstructorParameter(string greetSettingsType) + { + string source = $$""" + using System.Diagnostics.CodeAnalysis; + using Microsoft.Extensions.Configuration; + + public class Program + { + public static void Main() + { + ConfigurationBuilder configurationBuilder = new(); + IConfiguration config = configurationBuilder.Build(); + + GreetSettings settings = config.GetSection("Settings").Get()!; + } + } + + {{greetSettingsType}} + """; + + ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source, assemblyReferences: GetAssemblyRefsWithAdditional(typeof(ConfigurationBuilder))); + Assert.NotNull(result.GeneratedSource); + Assert.Empty(result.Diagnostics); + + AssertCanCreateAssemblyImage(result.OutputCompilation); } [Fact] @@ -322,10 +375,7 @@ public static void Main() Assert.NotNull(result.GeneratedSource); Assert.Empty(result.Diagnostics); - // Ensure the generated code can be compiled. - // If there is any compilation error, exception will be thrown with the list of the errors in the exception message. - byte[] emittedAssemblyImage = CreateAssemblyImage(result.OutputCompilation); - Assert.NotNull(emittedAssemblyImage); + AssertCanCreateAssemblyImage(result.OutputCompilation); } ///