Description
openedon Jan 11, 2024
Description
When a configuration doesn't contain anything for a collection property (in the below case the ResponseHeadersDataClasses
collection), the ConfigurationBinder reflection based implementation will still call the setter for the collection. But switching to the ConfigurationBinder source generator causes the setter to no longer be invoked.
Reproduction Steps
csproj:
<Project Sdk="Microsoft.NET.Sdk.Worker">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" />
</ItemGroup>
</Project>
appsettings.json:
{
"HttpLogging": {
"ExcludePathStartsWith": [
"path"
]
}
}
Program.cs:
var builder = Host.CreateApplicationBuilder(args);
builder.Configuration.GetSection("HttpLogging").Get<LoggingRedactionOptions>();
Console.WriteLine("Done");
public class LoggingRedactionOptions
{
private IDictionary<string, DataClassification> _responseHeadersDataClasses = new Dictionary<string, DataClassification>(StringComparer.OrdinalIgnoreCase);
public IDictionary<string, DataClassification> ResponseHeadersDataClasses
{
get { return _responseHeadersDataClasses; }
set
{
Console.WriteLine("ResponseHeadersDataClasses setter called!");
_responseHeadersDataClasses = value;
}
}
public ISet<string> ExcludePathStartsWith { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
}
public readonly struct DataClassification : IEquatable<DataClassification>
{
public string TaxonomyName { get; }
public string Value { get; }
public DataClassification(string taxonomyName, string value)
{
TaxonomyName = taxonomyName;
Value = value;
}
public override bool Equals(object? obj) => (obj is DataClassification dc) && Equals(dc);
public bool Equals(DataClassification other) => other.TaxonomyName == TaxonomyName && other.Value == Value;
public override int GetHashCode() => HashCode.Combine(TaxonomyName, Value);
public static bool operator ==(DataClassification left, DataClassification right)
{
return left.Equals(right);
}
public static bool operator !=(DataClassification left, DataClassification right)
{
return !left.Equals(right);
}
public override string ToString() => string.IsNullOrWhiteSpace(TaxonomyName) ? Value : $"{TaxonomyName}:{Value}";
}
Toggle the EnableConfigurationBindingGenerator
setting between true
and false
.
Expected behavior
The behavior of the app should be the same whether you are setting EnableConfigurationBindingGenerator
to false or true. It should print ResponseHeadersDataClasses setter called!
in both cases.
Actual behavior
When the Source Generator is not used:
ResponseHeadersDataClasses setter called!
Done
When the Source Generator is used:
Done
Other information
See this comment (and #80438) why the setter is being called in the non-SG case.
In the source generator case, the generated code looks like:
if (AsConfigWithChildren(configuration.GetSection("ResponseHeadersDataClasses")) is IConfigurationSection section2)
{
global::System.Collections.Generic.IDictionary<string, global::DataClassification>? temp4 = instance.ResponseHeadersDataClasses;
temp4 ??= (global::System.Collections.Generic.IDictionary<string, global::DataClassification>)new Dictionary<string, global::DataClassification>();
BindCore(section2, ref temp4, defaultValueIfNotFound: false, binderOptions);
instance.ResponseHeadersDataClasses = temp4;
}
So the setter is only ever called when a ResponseHeadersDataClasses
section with children is present in the configuration.