diff --git a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/BindingHelperInfo.cs b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/BindingHelperInfo.cs index 5cdc40e011517d..29e1c6c16cbea9 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/BindingHelperInfo.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/BindingHelperInfo.cs @@ -173,6 +173,14 @@ bool TryRegisterCore() { foreach (PropertySpec property in objectSpec.Properties!) { + // Skip types reachable only through non-bindable properties, unless the + // property backs a constructor parameter. Constructor parameters are always + // bound in the Initialize method, so their types must still be registered. + if (!_typeIndex.ShouldBindTo(property) && property.MatchingCtorParam is null) + { + continue; + } + TryRegisterTransitiveTypesForMethodGen(property.TypeRef); if (_typeIndex.GetTypeSpec(property.TypeRef) is ComplexTypeSpec) 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 47e3c33a15826f..1f69112c399616 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs @@ -349,6 +349,152 @@ public static void Main() AssertCanCreateAssemblyImage(result.OutputCompilation); } + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNetCore))] + public async Task ListOfTupleWithComplexElementInInternalPropertyTest() + { + string source = """ + using Microsoft.Extensions.Configuration; + using System; + using System.Collections.Generic; + + public class Program + { + public static void Main() + { + ConfigurationBuilder configurationBuilder = new(); + IConfiguration config = configurationBuilder.Build(); + ExampleOptions options = new(); + config.Bind(options); + } + } + + public class ExampleOptions + { + public List ExampleCollection { get; set; } = new(); + + internal List<(string, ICollection?)> UsesCollection => + [ + ("Label-1", ExampleCollection) + ]; + } + """; + + ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source, assemblyReferences: GetAssemblyRefsWithAdditional(typeof(ConfigurationBuilder), typeof(List<>))); + Assert.NotNull(result.GeneratedSource); + Assert.Empty(result.Diagnostics); + + AssertCanCreateAssemblyImage(result.OutputCompilation); + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNetCore))] + [InlineData("IReadOnlyList")] + [InlineData("IReadOnlyCollection")] + [InlineData("IReadOnlySet")] + [InlineData("IEnumerable")] + public async Task ReadOnlyCollectionConstructorParameterIsBindable(string collectionType) + { + string source = $$""" + using Microsoft.Extensions.Configuration; + using System.Collections.Generic; + + public class Program + { + public static void Main() + { + ConfigurationBuilder configurationBuilder = new(); + IConfiguration config = configurationBuilder.Build(); + Options options = config.Get(); + } + } + + public record Options(string Name, {{collectionType}} Values); + """; + + ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source, assemblyReferences: GetAssemblyRefsWithAdditional(typeof(ConfigurationBuilder), typeof(List<>))); + Assert.NotNull(result.GeneratedSource); + Assert.Empty(result.Diagnostics); + + // The collection type is only reachable through a read-only property, so its BindCore + // helper must still be generated for the constructor parameter. + AssertCanCreateAssemblyImage(result.OutputCompilation); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNetCore))] + public async Task ComplexReadOnlyListConstructorParameterIsBindable() + { + string source = """ + using Microsoft.Extensions.Configuration; + using System.Collections.Generic; + + public class Program + { + public static void Main() + { + ConfigurationBuilder configurationBuilder = new(); + IConfiguration config = configurationBuilder.Build(); + Options options = config.Get(); + } + } + + public class Child + { + public string Value { get; set; } + } + + public record Options(string Name, IReadOnlyList Items); + """; + + ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source, assemblyReferences: GetAssemblyRefsWithAdditional(typeof(ConfigurationBuilder), typeof(List<>))); + Assert.NotNull(result.GeneratedSource); + Assert.Empty(result.Diagnostics); + + AssertCanCreateAssemblyImage(result.OutputCompilation); + } + + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNetCore))] + public async Task TypeReachableOnlyThroughNonBindablePropertyIsNotEmitted() + { + string source = """ + using Microsoft.Extensions.Configuration; + using System.Collections.Generic; + + public class Program + { + public static void Main() + { + ConfigurationBuilder configurationBuilder = new(); + IConfiguration config = configurationBuilder.Build(); + ExampleOptions options = new(); + config.Bind(options); + } + } + + public class ExampleOptions + { + public List ExampleCollection { get; set; } = new(); + + // Non-bindable internal property. UnreachableChild is only reachable through it, + // so the generator must not emit any binding code that references it. + internal List UsesCollection => new(); + } + + public class UnreachableChild + { + public string Value { get; set; } + } + """; + + ConfigBindingGenRunResult result = await RunGeneratorAndUpdateCompilation(source, assemblyReferences: GetAssemblyRefsWithAdditional(typeof(ConfigurationBuilder), typeof(List<>))); + Assert.NotNull(result.GeneratedSource); + Assert.Empty(result.Diagnostics); + + // The type is reachable only through a non-bindable property, so the generator must + // not register or emit any binding code that references it. + Assert.DoesNotContain("UnreachableChild", result.GeneratedSource.Value.SourceText.ToString()); + + AssertCanCreateAssemblyImage(result.OutputCompilation); + } + [Fact] public async Task BindingToCollectionOnlyTest() {