Skip to content

ConfigurationBinder source generator generates non-compilable code with new property changing enum types #101273

Description

@eerhardt

dotnet build on the following projects results in the generated source from the Configuration.Binder source generator to not compile.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <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>
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

var builder = Host.CreateApplicationBuilder(args);

builder.Configuration.Bind(new CertificateDescription());

var host = builder.Build();
host.Run();


public enum CredentialSource
{
    Certificate = 0,
    KeyVault = 1,
    Base64Encoded = 2,
}
public enum CertificateSource
{
    Certificate = 0,
    KeyVault = 1,
}

public class CredentialDescription
{
    public CredentialSource SourceType { get; set; }
}

public class CertificateDescription : CredentialDescription
{
    // make a new property that limits the SourceType to CertificateSource
    public new CertificateSource SourceType
    {
        get { return (CertificateSource)base.SourceType; }
        set { base.SourceType = (CredentialSource)value; }
    }
}

Expected result

I expect the code to compile.

Actual result

Build FAILED.

C:\Users\eerhardt\source\repos\WorkerService20\WorkerService20\obj\Debug\net8.0\Microsoft.Extensions.Configuration.Binder.SourceGeneration\Microsoft.
Extensions.Configuration.Binder.SourceGeneration.ConfigurationBindingGenerator\BindingExtensions.g.cs(65,39): error CS0266: Cannot implicitly convert
 type 'CredentialSource' to 'CertificateSource'. An explicit conversion exists (are you missing a cast?) [C:\Users\eerhardt\source\repos\WorkerServic
e20\WorkerService20\WorkerService20.csproj]
    0 Warning(s)
    1 Error(s)

The problematic line is:

global::CertificateDescription instance;

instance.SourceType = ParseEnum<global::CredentialSource>(value0, () => configuration.GetSection("SourceType").Path);

instance.SourceType is a CertificateSource, but the code is parsing as a CredentialSource.

Generated code

// <auto-generated/>

#nullable enable annotations
#nullable disable warnings

// Suppress warnings about [Obsolete] member usage in generated code.
#pragma warning disable CS0612, CS0618

namespace System.Runtime.CompilerServices
{
    using System;
    using System.CodeDom.Compiler;

    [GeneratedCode("Microsoft.Extensions.Configuration.Binder.SourceGeneration", "8.0.9.7805")]
    [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
    file sealed class InterceptsLocationAttribute : Attribute
    {
        public InterceptsLocationAttribute(string filePath, int line, int column)
        {
        }
    }
}

namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
{
    using Microsoft.Extensions.Configuration;
    using System;
    using System.CodeDom.Compiler;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Runtime.CompilerServices;

    [GeneratedCode("Microsoft.Extensions.Configuration.Binder.SourceGeneration", "8.0.9.7805")]
    file static class BindingExtensions
    {
        #region IConfiguration extensions.
        /// <summary>Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively.</summary>
        [InterceptsLocation(@"C:\Users\eerhardt\source\repos\WorkerService20\WorkerService20\Program.cs", 6, 23)]
        public static void Bind_CertificateDescription(this IConfiguration configuration, object? instance)
        {
            if (configuration is null)
            {
                throw new ArgumentNullException(nameof(configuration));
            }

            if (instance is null)
            {
                return;
            }

            var typedObj = (global::CertificateDescription)instance;
            BindCore(configuration, ref typedObj, defaultValueIfNotFound: false, binderOptions: null);
        }
        #endregion IConfiguration extensions.

        #region Core binding extensions.
        private readonly static Lazy<HashSet<string>> s_configKeys_CertificateDescription = new(() => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "SourceType" });

        public static void BindCore(IConfiguration configuration, ref global::CertificateDescription instance, bool defaultValueIfNotFound, BinderOptions? binderOptions)
        {
            ValidateConfigurationKeys(typeof(global::CertificateDescription), s_configKeys_CertificateDescription, configuration, binderOptions);

            if (configuration["SourceType"] is string value0)
            {
                instance.SourceType = ParseEnum<global::CredentialSource>(value0, () => configuration.GetSection("SourceType").Path);
            }
            else if (defaultValueIfNotFound)
            {
                instance.SourceType = default;
            }
        }


        /// <summary>If required by the binder options, validates that there are no unknown keys in the input configuration object.</summary>
        public static void ValidateConfigurationKeys(Type type, Lazy<HashSet<string>> keys, IConfiguration configuration, BinderOptions? binderOptions)
        {
            if (binderOptions?.ErrorOnUnknownConfiguration is true)
            {
                List<string>? temp = null;
        
                foreach (IConfigurationSection section in configuration.GetChildren())
                {
                    if (!keys.Value.Contains(section.Key))
                    {
                        (temp ??= new List<string>()).Add($"'{section.Key}'");
                    }
                }
        
                if (temp is not null)
                {
                    throw new InvalidOperationException($"'ErrorOnUnknownConfiguration' was set on the provided BinderOptions, but the following properties were not found on the instance of {type}: {string.Join(", ", temp)}");
                }
            }
        }

        public static T ParseEnum<T>(string value, Func<string?> getPath) where T : struct
        {
            try
            {
                #if NETFRAMEWORK || NETSTANDARD2_0
                    return (T)Enum.Parse(typeof(T), value, ignoreCase: true);
                #else
                    return Enum.Parse<T>(value, ignoreCase: true);
                #endif
            }
            catch (Exception exception)
            {
                throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(T)}'.", exception);
            }
        }
        #endregion Core binding extensions.
    }
}

cc @ericstj @tarekgh @eiriktsarpalis

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions