Uh oh!
There was an error while loading. Please reload this page.
Fix nested readonly collection ctor parameters - #131932
Merged
rosebyte merged 2 commits intoAug 31, 2026
Merged
Conversation
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
Tagging subscribers to this area: @dotnet/area-extensions-configuration |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR fixes a configuration binding source-generator parity gap where certain nested complex members were skipped during generated binding, leaving them null/default instead of being constructed and populated as the reflection-based binder would.
Changes:
- Refactors the “should we emit binding code for this complex member?” decision into
IsBindableAsMemberand applies it consistently to bothEmitBindImplForMemberandIsPropertyReboundInBindCore. - Updates value-type complex-member binding to directly assign the instance created by
Initialize(...)when the type has no bindable members beyond constructor parameters. - Adds new regression tests covering nested binding for both reference types and value types (including nullable structs), plus a reflection-binder test for the same shape.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/libraries/Microsoft.Extensions.Configuration.Binder/tests/SourceGenerationTests/GeneratorTests.cs | Adds generator regression tests for nested binding of “sole read-only collection ctor parameter” shapes (class + struct). |
| src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.TestClasses.Collections.cs | Introduces test types used to validate nested binding behavior in reflection-binder tests. |
| src/libraries/Microsoft.Extensions.Configuration.Binder/tests/Common/ConfigurationBinderTests.Collections.cs | Adds a reflection-binder regression test verifying nested binding for the same type shapes. |
| src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs | Fixes emitter logic so nested members that bind via constructor parameters are no longer skipped; adds a value-type fast path to assign initialized instances. |
rosebyteforce-pushed
the
rosebyte-fix-nested-readonly-collection-ctor-para
branch
from
August 24, 2026 11:42
30ebc1e to
06beb5aComparerosebyte
marked this pull request as ready for review
August 24, 2026 11:43
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Uh oh!
There was an error while loading. Please reload this page.
svick
approved these changes
Aug 31, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#131399
Problem
The configuration binder source generator silently bound
null/defaultfor a nested member whose type's sole member is a constructor parameter of a read-only collection type:The reflection binder binds both correctly, so this was a silent divergence between the two engines: no diagnostic, no exception, just missing configuration at runtime.
#131358 fixed the equivalent top-level case (
config.Get<Inner>()). The nested case remained broken, and this is a pre-existing gap rather than a regression from that change.Root cause
Two independent bugs shared one symptom.
1. Reference types.
EmitBindImplForMemberskipped a parameterized-constructor object with no bindable members. But such a type binds its constructor parameters in the generatedInitializemethod regardless of whether it has properties forBindCoreto mutate. Thanks to #131358,InitializeInnerwas emitted andCanInstantiatewas true; the nested-member path simply never called it.2. Value types. Relaxing that guard is not sufficient for structs:
EmitBindingLogicForComplexMemberusesInitializationKind.Nonefor value types because a struct returned by a property getter is a copy and cannot be bound in place.EmitBindingLogicreturns immediately on!HasBindableMemberswhen the initialization kind isNone.The result was an inert temporary and a member left at
default, even thoughInitializeInnerStructcould create the correct value.Fix
The existing member predicate is extracted into
IsBindableAsMemberand extended with exactly one term:This is the old predicate plus
(canSet && CanInstantiate). Existing value-type and collection decisions remain unchanged, limiting generated-output changes to the affected constructor-bound shapes.IsPropertyReboundInBindCoreuses the same predicate. This is required for correctness, not merely deduplication: a constructor parameter can match a set-only property. If the two decisions diverge,Initializeconstructs the nested value for the parent constructor andBindCorethen emits a second unconditional assignment through that property. Sharing the predicate lets the existingboundThroughConstructormechanism skip the second assignment for newly constructed parents while retaining it forBind(existingInstance).For constructor-only value types,
EmitBindingLogicForComplexMembernow directly emits:There is nothing to bind in place, so this avoids the general temporary path and assigns the only meaningful result directly.
canSetThe new reference-type term requires
canSetbecause the generatedInitializeresult must be assigned to the member. Constructor-parameter locals passcanSet: true, so init-only positional-record members continue to bind through their matching constructor parameters. A get-only member with no bindable members remains skipped because there is nowhere to put a new instance.The pre-existing value-type predicate is deliberately preserved.
EmitBindingLogicForComplexMemberalready returns when a value-type member cannot be set; retaining the old predicate avoids unrelated generated-signature and dead-block changes for get-only struct members.Generated code
Before, for
class Outer { public Inner? Nested { get; set; } }:After:
The reference/value asymmetry follows the existing emitter model:
??=, preserving an instance already stored on the member.=. A struct getter returns a copy,??=is unavailable for non-nullable structs, and using it for a populated nullable struct would ignore a present configuration section because this type has noBindCorework to perform.Scope and performance
The fix does not change
HasBindableMembers,CanInstantiate, helper registration, or the general value-type initialization strategy. Unaffected shapes retain the old predicate result and generated path. Affected shapes add only the previously missing section lookup andInitializeassignment; parent types whose matching constructor properties now emit binding logic use the existingboundThroughConstructorbranch to prevent a second assignment.The extra generator-time
CanInstantiatecheck is reached only for the previously rejected parameterized-object shape and is an O(1) object-spec check. No runtime benchmark is meaningful here because the baseline silently omits the required binding work; the changed path necessarily performs that work.Tests
Coverage includes:
IReadOnlyDictionary<,>'sLinqToDictionarystrategy;Shared Common tests run under both reflection and source-generated binding to pin parity for the fixed fresh-binding scenarios.
Note
Parts of this description were drafted with GitHub Copilot.