Skip to content

Configuration SG: type whose only member is a non-bindable constructor parameter emits CS0103 #131320

Description

@rosebyte

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 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)

  1. Leave as-is / document. The scenario is rare; Initialize registration for a directly-bound sole-ctor-param type stays a known gap. Lowest risk.
  2. 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.
  3. 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, ShouldBindTo
  • src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Specs/BindingHelperInfo.cs:172 — registration gate + MatchingCtorParam exemption
  • src/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, ReadOnlyCollectionConstructorParameterIsBindable
  • src/libraries/System.Net.Security/ref/System.Net.Security.cs — CipherSuitesPolicy platform attributes

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions