Skip to content

Extensions.Configuration.Binder generated code fails on binding of IEnumerable<string> in a nested record. #123422

@dmitry-kandiner

Description

@dmitry-kandiner

Description

Binding fails with InvalidOperationException for an IEnumerable<string> field in a nested record:

internal sealed record Config
{
    public Source? Source { get; set; }
}
internal sealed record Source(string Name, IEnumerable<string> Addresses); // Fails on binding of Addresses

Reproduction Steps

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;

// Simulated configuration file content
var config = """
{
  "source": {
    "name": "DemoService",
    "addresses": [ "127.0.0.1" ]
  }
}
""";

// Steps to reproduce
var builder = Host.CreateApplicationBuilder(args);
builder.Configuration.AddJsonStream(new MemoryStream(System.Text.Encoding.UTF8.GetBytes(config)));

builder.Services.AddSingleton<DemoService>();

builder.Services.Configure<Config>(builder.Configuration);

var app = builder.Build();

await app.Services.GetRequiredService<DemoService>().StartAsync(new CancellationToken());

app.Run();

// Configuration structure that fails on binding
internal sealed record Config
{
    public Source? Source { get; set; }
}
internal sealed record Source(string Name, IEnumerable<string> Addresses);

// Background service that uses the configuration
internal class DemoService(IOptionsMonitor<Config> config) : BackgroundService
{
    internal string Name { get; } = config.CurrentValue.Source?.Name ?? "DefaultName";
    // This line throws an InvalidOperationException during binding
    internal IEnumerable<string> Addresses { get; } = config.CurrentValue.Source?.Addresses ?? [];

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        await Task.Delay(Timeout.Infinite, stoppingToken);
    }
}

Expected behavior

Config.Source.Addresses is bound correctly.

Actual behavior

Accessing Config.Source?.Addresses fails with InvalidOperationException:

Here is a snipped from the generated file:

            global::System.Collections.Generic.IEnumerable<string> Addresses = default!;
            var value5 = configuration.GetSection("Addresses");
            if (AsConfigWithChildren(value5) is IConfigurationSection section6)
            {
                Addresses = (global::System.Collections.Generic.IEnumerable<string>)new List<string>();
                BindCore(section6, ref Addresses, defaultValueIfNotFound: false, binderOptions);
                
            }
            if (Addresses is null && TryGetConfigurationValue(value5, key: null, out string? value9) && value9 == string.Empty)
            {
                Addresses = global::System.Array.Empty<string>();
            }
            else
            {
                throw new InvalidOperationException("Cannot create instance of type 'Source' because parameter 'Addresses' has no matching config. Each parameter in the constructor that does not have a default value must have a corresponding config entry.");
            }

The exception is thrown from the else statement.

Regression?

The same code worked with Microsoft.Extensions.Hosting NuGET in versions 9.0.12 and 10.0.0

Apparently, the newly generated code is

            if (Addresses is null && TryGetConfigurationValue(value5, key: null, out string? value9) && value9 == string.Empty)
            {
                Addresses = global::System.Array.Empty<string>();
            }

It was not generated by version 10.0.0 of the NuGET

Known Workarounds

No response

Configuration

.NET10 (10.0.2)
Windows 11 Pro 25H2 Build 26200.7623
x64

Other information

No response

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions