From d97c7f22637840b6ee4784344f9a4353395f1452 Mon Sep 17 00:00:00 2001 From: Petr Onderka Date: Fri, 10 Jul 2026 14:47:04 +0200 Subject: [PATCH 1/2] Fix config binding generator CS0103 for required/init property with matching ctor param The generated Initialize method re-assigns required/init properties that also flow through a matching constructor parameter, so custom init/set accessors run after the constructor. It used the property name as the value expression (`Name = Name`), but the bound value lives in the local named after the constructor parameter. When the parameter name differed from the property (e.g. only in casing, `name` vs `Name`), no such local existed and the generated code failed to compile with CS0103. Use the matching constructor parameter's local name as the assigned value when present, falling back to the property name otherwise. Output is unchanged when the parameter and property names match exactly (e.g. positional records), so no baselines change. Fixes #128390 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e830d4aa-6a35-4105-9979-86d92e5255d2 --- .../gen/Emitter/CoreBindingHelpers.cs | 6 +- .../ConfigurationBinderTests.TestClasses.cs | 21 +++++++ .../tests/Common/ConfigurationBinderTests.cs | 18 ++++++ .../SourceGenerationTests/GeneratorTests.cs | 59 +++++++++++++++++++ 4 files changed, 102 insertions(+), 2 deletions(-) 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.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs index d74ac253e616eb..d810df5d393ced 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs @@ -296,6 +296,65 @@ public class Settings Assert.NotNull(emittedAssemblyImage); } + [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); + + // 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); + } + [Fact] public async Task BindingToCollectionOnlyTest() { From b2d89f5c58e820f3ca384e83924f193eaea980a8 Mon Sep 17 00:00:00 2001 From: Petr Onderka Date: Fri, 10 Jul 2026 14:53:03 +0200 Subject: [PATCH 2/2] Simplify checking for compilation emit success --- .../GeneratorTests.Helpers.cs | 6 ++---- .../tests/SourceGenerationTests/GeneratorTests.cs | 15 +++------------ 2 files changed, 5 insertions(+), 16 deletions(-) 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 d810df5d393ced..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,7 @@ 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))] @@ -349,10 +346,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); } [Fact] @@ -381,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); } ///