Repository navigation
Configuration SG: type whose only member is a non-bindable constructor parameter emits CS0103 #131320
Description
Originates from @tarekgh's review comment on PR #130119:
#130119 (comment)
Summary
The configuration binder source generator emits uncompilable code for a parameterized-constructor type whose only member is a non-bindable copy-constructor collection parameter (e.g. a positional record with a single IReadOnlyList<T> / IReadOnlyCollection<T> / IReadOnlySet<T> / IEnumerable<T> parameter and no other bindable property). The generated code calls an InitializeOptions(...) helper that is never generated, producing:
error CS0103: The name 'InitializeOptions' does not exist in the current context
The reflection binder handles this shape correctly, so the source generator is a parity regression here (it breaks the build instead).
Repro
using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
var config = new ConfigurationBuilder().Build();
Options options = config.Get<Options>(); // generated code fails to compile
public record Options(IReadOnlyList<string> Values);All four read-only interface shapes reproduce: IReadOnlyList<T>, IReadOnlyCollection<T>, IReadOnlySet<T>, IEnumerable<T>, as does a complex element type (record Options(IReadOnlyList<Child> Items)).
The existing PR #130119 tests (ReadOnlyCollectionConstructorParameterIsBindable, ComplexReadOnlyListConstructorParameterIsBindable) do not catch this because each declares an additional bindable parameter (string Name), which makes HasBindableMembers return true and masks the gap.
Root cause
TypeIndex.HasBindableMembers(ObjectSpec)is defined asProperties.Any(ShouldBindTo)(gen/Specs/TypeIndex.cs). For this shape the sole property is the{ get; init; }read-only-collection copy-constructor parameter, which is non-bindable (ShouldBindTo == false,CanSet == false), soHasBindableMembersisfalse.- In
BindingHelperInfo.TryRegisterTransitiveTypesForMethodGen(theObjectSpeccase,gen/Specs/BindingHelperInfo.cs:172) the entire registration block is gated onHasBindableMembers, includingRegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.Initialize, objectSpec). SoInitializeis never registered. - The emitter nevertheless emits an
InitializeOptions(...)call for the parameterized-constructor instantiation (EmitInitializeMethodbinds all constructor parameters unconditionally), producing a call to a helper that was never generated → CS0103.
This is exactly the case @tarekgh flagged: the MatchingCtorParam is null exemption that PR #130119 added lives inside the HasBindableMembers block, so it never runs when the type has no bindable properties.
Why the obvious fix is not safe
Widening HasBindableMembers (or the registration gate) to also count bound constructor parameters does fix the CS0103, but it simultaneously flips the nested "early-out" gate in gen/Emitter/CoreBindingHelpers.cs:961, which deliberately skips parameterized-constructor types that have no bindable properties. That gate exists to avoid emitting new T(...) for types such as System.Net.Security.CipherSuitesPolicy — which is annotated [UnsupportedOSPlatform("windows")] and [UnsupportedOSPlatform("android")]. Emitting its construction into user code trips CA1416 (platform compatibility) → build break for Windows/Android consumers. This is guarded by IgnoredUnBindablePropertiesTest.
In other words, the sole-constructor-parameter gap and the deliberate exclusion of platform-restricted (and otherwise "unbindable-shaped") types are the same code shape. Any fix must resolve the CS0103 without re-enabling emission of construction for types like CipherSuitesPolicy.
The reflection binder avoids the dilemma because it has no compile-time platform analysis: it constructs CipherSuitesPolicy via its single IEnumerable<TlsCipherSuite> constructor when config provides it, and simply throws PlatformNotSupportedException at runtime on unsupported OSes (CreateInstance in src/.../ConfigurationBinder.cs).
Candidate approaches (to discuss)
- Leave as-is / document. The scenario is rare;
Initializeregistration for a directly-bound sole-ctor-param type stays a known gap. Lowest risk. - Fix only directly-bound (root) types. Register
Initialize/BindCorewhen such a type is the target of aGet<T>/Bind<T>invocation, while keeping the nestedCoreBindingHelpers.cs:961early-out so platform-restricted nested members (CipherSuitesPolicy) remain skipped. Residual: explicitly binding a platform-restricted type at the root would still emit a CA1416-affected construction. - Skip only genuinely-unbindable/platform-restricted types. Detect
[UnsupportedOSPlatform](and similar) and skip those specifically, binding all other sole-ctor-param types. Largest change; closest to reflection parity.
References
- Origin: PR Fix config binder source generator emitting invalid tuple instantiation code #130119 review comment Fix config binder source generator emitting invalid tuple instantiation code #130119 (comment)
src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/TypeIndex.cs—HasBindableMembers,ShouldBindTosrc/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/BindingHelperInfo.cs:172— registration gate +MatchingCtorParamexemptionsrc/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs:961— nested early-out gate;EmitInitializeMethod(binds all ctor params)src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs—IgnoredUnBindablePropertiesTest,ReadOnlyCollectionConstructorParameterIsBindablesrc/libraries/System.Net.Security/ref/System.Net.Security.cs—CipherSuitesPolicyplatform attributes
Activity
dotnet-policy-service commentedon Jul 24, 2026
Tagging subscribers to this area: @dotnet/area-extensions-configuration
See info in area-owners.md if you want to be subscribed.
Fix config binder source gen for a sole read-only collection ctor par…