Repository navigation
Description
Description
Split out of review discussion on #131358.
If a bound type has a member whose own type's sole member is a read-only collection constructor parameter (IReadOnlyList<T>, IReadOnlyCollection<T>, IReadOnlySet<T>, or IEnumerable<T>, with no other bindable property), the source generator binds that nested member to null instead of the real value. No exception, no diagnostic. Top-level use of the same shape (the one #131358 fixes) works correctly; only the nested case, reached through BindCore/EmitObjectInit instead of GetCore, is affected.
Reproduction steps
using Microsoft.Extensions.Configuration;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
ConfigurationBuilder configurationBuilder = new();
configurationBuilder.AddInMemoryCollection(new Dictionary<string, string?>
{
["Nested:Values:0"] = "a",
["Nested:Values:1"] = "b",
});
IConfiguration config = configurationBuilder.Build();
Outer outer = config.Get<Outer>();
// outer.Nested is non-null, but outer.Nested.Values is null.
System.Console.WriteLine(outer.Nested.Values is null ? "Values is null" : "Values is bound");
}
}
public record Inner(IReadOnlyList<string> Values);
public record Outer(Inner Nested);Needs a project with the config binder source generator active (AOT/trimming context, or EnableConfigurationBindingGenerator=true).
Expected behavior
outer.Nested.Values contains ["a", "b"], matching what reflection-based binding (Microsoft.Extensions.Configuration.Binder without the generator) produces for the same shape.
Actual behavior
outer.Nested.Values is null, with no exception or generator diagnostic.
Root cause
In src/libraries/Microsoft.Extensions.Configuration.Binder/gen/Emitter/CoreBindingHelpers.cs, EmitBindImplForMember's early-return guard:
if (!_typeIndex.HasBindableMembers(complexType) &&
!_typeIndex.GetEffectiveTypeSpec(complexType).IsValueType &&
complexType is not CollectionSpec &&
((ObjectSpec)complexType).InstantiationStrategy == ObjectInstantiationStrategy.ParameterizedConstructor)
{
return false;
}skips emitting the member assignment whenever HasBindableMembers is false, without accounting for types that are constructible via ParameterizedConstructor even without other bindable members (the same condition #131358 fixes for the top-level/GetCore path). Initialize<Inner> is generated correctly and CanInstantiate is true for Inner, but nothing ever calls it here, so the member silently stays at its default (null).
Confirmed via git diff against the commit before #131358 that this code path is untouched by that PR, so it's a pre-existing gap, not a regression.
Regression?
No, pre-existing.
Other information
Found while adding nested-usage test coverage during review of #131358, which fixes the equivalent top-level (GetCore) case for the same "sole read-only collection ctor param" shape. This issue tracks the BindCore/EmitObjectInit (nested) path separately, per @tarekgh's request on that PR.
Activity
Give each LoadAndInvokeMain compilation a unique assembly name
dotnet-policy-service commentedon Jul 26, 2026
Tagging subscribers to this area: @dotnet/area-extensions-configuration
See info in area-owners.md if you want to be subscribed.
Fix nested readonly collection ctor parameters (#131932)