You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
usingMicrosoft.Extensions.Configuration;usingSystem.Collections.Generic;varconfig=newConfigurationBuilder().Build();Optionsoptions=config.Get<Options>();// generated code fails to compilepublicrecordOptions(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 as Properties.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), so HasBindableMembers is false.
In BindingHelperInfo.TryRegisterTransitiveTypesForMethodGen (the ObjectSpec case, gen/Specs/BindingHelperInfo.cs:172) the entire registration block is gated on HasBindableMembers, including RegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.Initialize, objectSpec). So Initialize is never registered.
The emitter nevertheless emits an InitializeOptions(...) call for the parameterized-constructor instantiation (EmitInitializeMethod binds 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; Initialize registration for a directly-bound sole-ctor-param type stays a known gap. Lowest risk.
Fix only directly-bound (root) types. Register Initialize/BindCore when such a type is the target of a Get<T> / Bind<T> invocation, while keeping the nested CoreBindingHelpers.cs:961 early-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.
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 anInitializeOptions(...)helper that is never generated, producing:The reflection binder handles this shape correctly, so the source generator is a parity regression here (it breaks the build instead).
Repro
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 makesHasBindableMembersreturntrueand 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.BindingHelperInfo.TryRegisterTransitiveTypesForMethodGen(theObjectSpeccase,gen/Specs/BindingHelperInfo.cs:172) the entire registration block is gated onHasBindableMembers, includingRegisterTypeForMethodGen(MethodsToGen_CoreBindingHelper.Initialize, objectSpec). SoInitializeis never registered.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 nullexemption that PR #130119 added lives inside theHasBindableMembersblock, 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 ingen/Emitter/CoreBindingHelpers.cs:961, which deliberately skips parameterized-constructor types that have no bindable properties. That gate exists to avoid emittingnew T(...)for types such asSystem.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 byIgnoredUnBindablePropertiesTest.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
CipherSuitesPolicyvia its singleIEnumerable<TlsCipherSuite>constructor when config provides it, and simply throwsPlatformNotSupportedExceptionat runtime on unsupported OSes (CreateInstanceinsrc/.../ConfigurationBinder.cs).Candidate approaches (to discuss)
Initializeregistration for a directly-bound sole-ctor-param type stays a known gap. Lowest risk.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.[UnsupportedOSPlatform](and similar) and skip those specifically, binding all other sole-ctor-param types. Largest change; closest to reflection parity.References
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