Skip to content

ConfigurationBinder Source Generator doesn't call setters in same way reflection based ConfigurationBinder does #96873

Closed

Description

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.

// For property binding, there are some cases when HasNewValue is not set in BindingPoint while a non-null Value inside that object can be retrieved from the property getter.
// As example, when binding a property which not having a configuration entry matching this property and the getter can initialize the Value.
// It is important to call the property setter as the setters can have a logic adjusting the Value.
if (!propertyBindingPoint.IsReadOnly && propertyBindingPoint.Value is not null)
{
property.SetValue(instance, propertyBindingPoint.Value);

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

Labels

Priority:1Work that is critical for the release, but we could probably ship withoutarea-Extensions-Configurationin-prThere is an active PR which will close this issue when it is mergedsource-generatorIndicates an issue with a source generator feature

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions