Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable configuration binding generator in TodosApi app #1874

Merged
merged 4 commits into from
Jul 21, 2023

Conversation

layomia
Copy link
Contributor

@layomia layomia commented Jun 27, 2023

Generated code (click to view)
// <auto-generated/>
#nullable enable

/// <summary>Generated helper providing an AOT and linking compatible implementation for configuration binding.</summary>
internal static class GeneratedOptionsBuilderBinder
{
    /// <summary>Registers the dependency injection container to bind <typeparamref name="TOptions"/> against the <see cref="global::Microsoft.Extensions.Configuration.IConfiguration"/> obtained from the DI service provider.</summary>
    public static global::Microsoft.Extensions.Options.OptionsBuilder<TOptions> BindConfiguration<TOptions>(this global::Microsoft.Extensions.Options.OptionsBuilder<TOptions> optionsBuilder, string configSectionPath, global::System.Action<global::Microsoft.Extensions.Configuration.BinderOptions>? configureOptions = null) where TOptions : class
    {
        if (optionsBuilder is null)
        {
            throw new global::System.ArgumentNullException(nameof(optionsBuilder));
        }

        if (configSectionPath is null)
        {
            throw new global::System.ArgumentNullException(nameof(configSectionPath));
        }

        optionsBuilder.Configure<global::Microsoft.Extensions.Configuration.IConfiguration>((obj, configuration) =>
        {
            global::Microsoft.Extensions.Configuration.IConfiguration section = string.Equals(string.Empty, configSectionPath, global::System.StringComparison.OrdinalIgnoreCase) ? configuration : configuration.GetSection(configSectionPath);
            global::Microsoft.Extensions.Configuration.Binder.SourceGeneration.CoreBindingHelper.BindCoreUntyped(section, obj, typeof(TOptions), configureOptions);
        });

        global::Microsoft.Extensions.DependencyInjection.ServiceCollectionServiceExtensions.AddSingleton<global::Microsoft.Extensions.Options.IOptionsChangeTokenSource<TOptions>, global::Microsoft.Extensions.Options.ConfigurationChangeTokenSource<TOptions>>(optionsBuilder.Services);
        return optionsBuilder;
    }
}

namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
{
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using System;
    using System.Collections.Generic;
    using System.Globalization;
    using TodosApi;

    /// <summary>Provide core binding logic.</summary>
    internal static class CoreBindingHelper
    {
        public static void BindCoreUntyped(this IConfiguration configuration, object obj, Type type, Action<BinderOptions>? configureOptions)
        {
            if (configuration is null)
            {
                throw new ArgumentNullException(nameof(configuration));
            }

            BinderOptions? binderOptions = GetBinderOptions(configureOptions);

            if (!HasValueOrChildren(configuration))
            {
                return;
            }

            if (type == typeof(AppSettings))
            {
                var temp = (AppSettings)obj;
                BindCore(configuration, ref temp, binderOptions);
                return;
            }

            throw new global::System.NotSupportedException($"Unable to bind to type '{type}': generator did not detect the type as input.");
        }

        public static void BindCore(IConfiguration configuration, ref AppSettings obj, BinderOptions? binderOptions)
        {
            if (obj is null)
            {
                throw new ArgumentNullException(nameof(obj));
            }

            List<string>? temp = null;
            foreach (IConfigurationSection section in configuration.GetChildren())
            {
                switch (section.Key)
                {
                    case "ConnectionString":
                        {
                            obj.ConnectionString = configuration["ConnectionString"]!;
                        }
                        break;
                    case "JwtSigningKey":
                        {
                            obj.JwtSigningKey = configuration["JwtSigningKey"]!;
                        }
                        break;
                    case "SuppressDbInitialization":
                        {
                            if (configuration["SuppressDbInitialization"] is string stringValue3)
                            {
                                obj.SuppressDbInitialization = ParseBool(stringValue3, () => section.Path)!;
                            }
                        }
                        break;
                    default:
                        {
                            if (binderOptions?.ErrorOnUnknownConfiguration == true)
                            {
                                (temp ??= new List<string>()).Add($"'{section.Key}'");
                            }
                        }
                        break;
                }
            }

            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 {typeof(AppSettings)}: {string.Join(", ", temp)}");
            }
        }

        public static bool HasValueOrChildren(IConfiguration configuration)
        {
            if ((configuration as IConfigurationSection)?.Value is not null)
            {
                return true;
            }
            return HasChildren(configuration);
        }

        public static bool HasChildren(IConfiguration configuration)
        {
            foreach (IConfigurationSection section in configuration.GetChildren())
            {
                return true;
            }
            return false;
        }

        public static BinderOptions? GetBinderOptions(Action<BinderOptions>? configureOptions)
        {
            if (configureOptions is null)
            {
                return null;
            }
            BinderOptions binderOptions = new();
            configureOptions(binderOptions);
            if (binderOptions.BindNonPublicProperties)
            {
                throw new global::System.NotSupportedException($"The configuration binding source generator does not support 'BinderOptions.BindNonPublicProperties'.");
            }
            return binderOptions;
        }

        public static bool ParseBool(string stringValue, Func<string?> getPath)
        {
            try
            {
                return bool.Parse(stringValue);
            }
            catch (Exception exception)
            {
                throw new InvalidOperationException($"Failed to convert configuration value at '{getPath()}' to type '{typeof(bool)}'.", exception);
            }
        }
    }
}

The binding extension classes in Microsoft.Extensions.Options.ConfigurationExtensions are now trimmed out from the app.

When dotnet/runtime#87935 flows through, ConfigurationBinder itself will be trimmed out.

@layomia
Copy link
Contributor Author

layomia commented Jun 27, 2023

cc @DamianEdwards @eerhardt.

@eerhardt
Copy link
Contributor

We should also remove this workaround:

# workaround https://github.com/dotnet/runtime/issues/86654 by disabling ConfigurationBindingGenerator
arguments: --application.buildArguments \"/p:PublishAot=true /p:StripSymbols=true /p:EnableConfigurationBindingGenerator=false\" --property mode=Aot --application.packageReferences \"Microsoft.Dotnet.ILCompiler=$(MicrosoftNETCoreAppPackageVersion)\"

@eerhardt
Copy link
Contributor

Looks like we can't make this change until dotnet/runtime#88112 is fixed.

@layomia
Copy link
Contributor Author

layomia commented Jul 19, 2023

@eerhardt I'm planning to fix the merge conflict; would that interrupt any work you're doing here?

@eerhardt
Copy link
Contributor

@eerhardt I'm planning to fix the merge conflict; would that interrupt any work you're doing here?

I rebased on main yesterday and resolved the merge conflict. I think just the above comment needs to be resolved, then this change can be merged.

@eerhardt eerhardt marked this pull request as ready for review July 21, 2023 16:13
@eerhardt eerhardt merged commit f5c3d1b into aspnet:main Jul 21, 2023
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants