Skip to content

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

Closed
@eerhardt

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions