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
Description
Binding fails with
InvalidOperationExceptionfor anIEnumerable<string>field in a nested record:Reproduction Steps
Expected behavior
Config.Source.Addressesis bound correctly.Actual behavior
Accessing
Config.Source?.Addressesfails withInvalidOperationException:Here is a snipped from the generated file:
The exception is thrown from the
elsestatement.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
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