From 5a7753874b5420a6b82edbfbf8b5d4e5b4736b2b Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Fri, 8 Nov 2024 15:08:01 -0600 Subject: [PATCH 1/4] Update configuration source generator documentation to use a class to bind the config values. --- .../extensions/configuration-generator.md | 136 +++++++++--------- .../console-binder-gen/Program.cs | 18 ++- 2 files changed, 82 insertions(+), 72 deletions(-) diff --git a/docs/core/extensions/configuration-generator.md b/docs/core/extensions/configuration-generator.md index ef4afb953b114..8b0e549d95c99 100644 --- a/docs/core/extensions/configuration-generator.md +++ b/docs/core/extensions/configuration-generator.md @@ -47,9 +47,9 @@ The preceding code: - Instantiates a configuration builder instance. - Calls and defines three configuration source values. - Calls to build the configuration. -- Uses the extension method to get the value for each configuration key. +- Uses the method to bind the `Settings` object to the configuration values. -When the application is built, the configuration source generator intercepts the call to `GetValue` and generates the binding code. +When the application is built, the configuration source generator intercepts the call to `Bind` and generates the binding code. > [!IMPORTANT] > When the `PublishAot` property is set to `true` (or any other AOT warnings are enabled) and the `EnabledConfigurationBindingGenerator` property is set to `false`, warning `IL2026` is raised. This warning indicates that members are attributed with [RequiresUnreferencedCode](xref:System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute) may break when trimming. For more information, see [IL2026](../deploying/trimming/trim-warnings/il2026.md). @@ -72,13 +72,11 @@ namespace System.Runtime.CompilerServices using System; using System.CodeDom.Compiler; - [GeneratedCode( - "Microsoft.Extensions.Configuration.Binder.SourceGeneration", - "8.0.10.31311")] + [GeneratedCode("Microsoft.Extensions.Configuration.Binder.SourceGeneration", "9.0.10.47305")] [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] file sealed class InterceptsLocationAttribute : Attribute { - public InterceptsLocationAttribute(string filePath, int line, int column) + public InterceptsLocationAttribute(int version, string data) { } } @@ -89,107 +87,113 @@ 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.10.31311")] + [GeneratedCode("Microsoft.Extensions.Configuration.Binder.SourceGeneration", "9.0.10.47305")] file static class BindingExtensions { #region IConfiguration extensions. - /// - /// Extracts the value with the specified key and converts it to the specified type. - /// - [InterceptsLocation(@"C:\source\configuration\console-binder-gen\Program.cs", 12, 26)] - [InterceptsLocation(@"C:\source\configuration\console-binder-gen\Program.cs", 13, 29)] - [InterceptsLocation(@"C:\source\configuration\console-binder-gen\Program.cs", 14, 28)] - public static T? GetValue(this IConfiguration configuration, string key) => - (T?)(BindingExtensions.GetValueCore(configuration, typeof(T), key) ?? default(T)); + /// Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively. + [InterceptsLocation(1, "uDIs2gDFz/yEvxOzjNK4jnIBAABQcm9ncmFtLmNz")] // D:\source\WorkerService1\WorkerService1\Program.cs(13,15) + public static void Bind_Settings(this IConfiguration configuration, object? instance) + { + ArgumentNullException.ThrowIfNull(configuration); + + if (instance is null) + { + return; + } + + var typedObj = (global::Settings)instance; + BindCore(configuration, ref typedObj, defaultValueIfNotFound: false, binderOptions: null); + } #endregion IConfiguration extensions. #region Core binding extensions. - public static object? GetValueCore( - this IConfiguration configuration, Type type, string key) + private readonly static Lazy> s_configKeys_Settings = new(() => new HashSet(StringComparer.OrdinalIgnoreCase) { "Port", "Enabled", "ApiUrl" }); + + public static void BindCore(IConfiguration configuration, ref global::Settings instance, bool defaultValueIfNotFound, BinderOptions? binderOptions) { - if (configuration is null) + ValidateConfigurationKeys(typeof(global::Settings), s_configKeys_Settings, configuration, binderOptions); + + if (configuration["Port"] is string value0 && !string.IsNullOrEmpty(value0)) { - throw new ArgumentNullException(nameof(configuration)); + instance.Port = ParseInt(value0, configuration.GetSection("Port").Path); } - - IConfigurationSection section = configuration.GetSection(key); - - if (section.Value is not string value) + else if (defaultValueIfNotFound) { - return null; + instance.Port = instance.Port; } - if (type == typeof(int)) + if (configuration["Enabled"] is string value1 && !string.IsNullOrEmpty(value1)) { - return ParseInt(value, () => section.Path); + instance.Enabled = ParseBool(value1, configuration.GetSection("Enabled").Path); } - else if (type == typeof(bool)) + else if (defaultValueIfNotFound) { - return ParseBool(value, () => section.Path); + instance.Enabled = instance.Enabled; } - else if (type == typeof(global::System.Uri)) + + if (configuration["ApiUrl"] is string value2) { - return ParseSystemUri(value, () => section.Path); + instance.ApiUrl = value2; + } + else if (defaultValueIfNotFound) + { + var currentValue = instance.ApiUrl; + if (currentValue is not null) + { + instance.ApiUrl = currentValue; + } } - - return null; } - public static int ParseInt( - string value, Func getPath) + + /// If required by the binder options, validates that there are no unknown keys in the input configuration object. + public static void ValidateConfigurationKeys(Type type, Lazy> keys, IConfiguration configuration, BinderOptions? binderOptions) { - try + if (binderOptions?.ErrorOnUnknownConfiguration is true) { - return int.Parse( - value, - NumberStyles.Integer, - CultureInfo.InvariantCulture); - } - catch (Exception exception) - { - throw new InvalidOperationException( - $"Failed to convert configuration value at " + - "'{getPath()}' to type '{typeof(int)}'.", - exception); + List? temp = null; + + foreach (IConfigurationSection section in configuration.GetChildren()) + { + if (!keys.Value.Contains(section.Key)) + { + (temp ??= new List()).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 bool ParseBool( - string value, Func getPath) + public static int ParseInt(string value, string? path) { try { - return bool.Parse(value); + return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture); } catch (Exception exception) { - throw new InvalidOperationException( - $"Failed to convert configuration value at " + - "'{getPath()}' to type '{typeof(bool)}'.", - exception); + throw new InvalidOperationException($"Failed to convert configuration value at '{path}' to type '{typeof(int)}'.", exception); } } - public static global::System.Uri ParseSystemUri( - string value, Func getPath) + public static bool ParseBool(string value, string? path) { try { - return new Uri( - value, - UriKind.RelativeOrAbsolute); + return bool.Parse(value); } catch (Exception exception) { - throw new InvalidOperationException( - $"Failed to convert configuration value at " + - "'{getPath()}' to type '{typeof(global::System.Uri)}'.", - exception); + throw new InvalidOperationException($"Failed to convert configuration value at '{path}' to type '{typeof(bool)}'.", exception); } } #endregion Core binding extensions. @@ -200,9 +204,9 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration > [!NOTE] > This generated code is subject to change based on the version of the configuration source generator. -The generated code contains the `BindingExtensions` class, which contains the `GetValueCore` method that performs the actual binding. The `GetValue` extension method calls the `GetValueCore` method and casts the result to the specified type. +The generated code contains the `BindingExtensions` class, which contains the `BindCore` method that performs the actual binding. The `Bind_Settings` method calls the `BindCore` method and casts the instance to the specified type. -To see the generated code, set the `true` in the project file. This ensures that the files are visible to the developer for inspection. +To see the generated code, set the `true` in the project file. This ensures that the files are visible to the developer for inspection. You can also view the generated code in Visual Studio's Solution Explorer under your project's `Dependencies` -> `Analyzers` -> `Microsoft.Extensions.Configuration.Binder.SourceGeneration` node. ## See also diff --git a/docs/core/extensions/snippets/configuration/console-binder-gen/Program.cs b/docs/core/extensions/snippets/configuration/console-binder-gen/Program.cs index a03e85d822cea..1cf6656c222d2 100644 --- a/docs/core/extensions/snippets/configuration/console-binder-gen/Program.cs +++ b/docs/core/extensions/snippets/configuration/console-binder-gen/Program.cs @@ -9,14 +9,20 @@ var configuration = builder.Build(); -var port = configuration.GetValue("port"); -var enabled = configuration.GetValue("enabled"); -var apiUrl = configuration.GetValue("apiUrl"); +var settings = new Settings(); +configuration.Bind(settings); // Write the values to the console. -Console.WriteLine($"Port = {port}"); -Console.WriteLine($"Enabled = {enabled}"); -Console.WriteLine($"API URL = {apiUrl}"); +Console.WriteLine($"Port = {settings.Port}"); +Console.WriteLine($"Enabled = {settings.Enabled}"); +Console.WriteLine($"API URL = {settings.ApiUrl}"); + +class Settings +{ + public int Port { get; set; } + public bool Enabled { get; set; } + public string? ApiUrl { get; set; } +} // This will output the following: // Port = 5001 From 48251284aeca4cdbd25d98391c79723e5d2a23b9 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Fri, 8 Nov 2024 17:00:11 -0600 Subject: [PATCH 2/4] Update docs/core/extensions/configuration-generator.md Co-authored-by: Genevieve Warren <24882762+gewarren@users.noreply.github.com> --- docs/core/extensions/configuration-generator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/extensions/configuration-generator.md b/docs/core/extensions/configuration-generator.md index 8b0e549d95c99..d260f013fd29f 100644 --- a/docs/core/extensions/configuration-generator.md +++ b/docs/core/extensions/configuration-generator.md @@ -206,7 +206,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration The generated code contains the `BindingExtensions` class, which contains the `BindCore` method that performs the actual binding. The `Bind_Settings` method calls the `BindCore` method and casts the instance to the specified type. -To see the generated code, set the `true` in the project file. This ensures that the files are visible to the developer for inspection. You can also view the generated code in Visual Studio's Solution Explorer under your project's `Dependencies` -> `Analyzers` -> `Microsoft.Extensions.Configuration.Binder.SourceGeneration` node. +To see the generated code, set the `true` in the project file. This ensures that the files are visible to the developer for inspection. You can also view the generated code in Visual Studio's Solution Explorer under your project's **Dependencies** > **Analyzers** > `Microsoft.Extensions.Configuration.Binder.SourceGeneration` node. ## See also From d08e7985ea2cab36553c300b07103773ffd95887 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Fri, 8 Nov 2024 17:21:19 -0600 Subject: [PATCH 3/4] Fix highlighted line --- docs/core/extensions/configuration-generator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/extensions/configuration-generator.md b/docs/core/extensions/configuration-generator.md index d260f013fd29f..5e6541f1102d1 100644 --- a/docs/core/extensions/configuration-generator.md +++ b/docs/core/extensions/configuration-generator.md @@ -40,7 +40,7 @@ The preceding project file enables the configuration source generator by setting Next, consider the _Program.cs_ file: -:::code source="snippets/configuration/console-binder-gen/Program.cs" highlight="12-14"::: +:::code source="snippets/configuration/console-binder-gen/Program.cs" highlight="13"::: The preceding code: From 840c6bfd2bbbd6589ae28fcc26c0ef201d5a890e Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 11 Nov 2024 10:24:35 -0600 Subject: [PATCH 4/4] Fix up some quotes --- docs/core/extensions/configuration-generator.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/core/extensions/configuration-generator.md b/docs/core/extensions/configuration-generator.md index 5e6541f1102d1..fb4a0cb54c214 100644 --- a/docs/core/extensions/configuration-generator.md +++ b/docs/core/extensions/configuration-generator.md @@ -206,7 +206,7 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration The generated code contains the `BindingExtensions` class, which contains the `BindCore` method that performs the actual binding. The `Bind_Settings` method calls the `BindCore` method and casts the instance to the specified type. -To see the generated code, set the `true` in the project file. This ensures that the files are visible to the developer for inspection. You can also view the generated code in Visual Studio's Solution Explorer under your project's **Dependencies** > **Analyzers** > `Microsoft.Extensions.Configuration.Binder.SourceGeneration` node. +To see the generated code, set the `true` in the project file. This ensures that the files are visible to the developer for inspection. You can also view the generated code in Visual Studio's **Solution Explorer** under your project's **Dependencies** > **Analyzers** > **Microsoft.Extensions.Configuration.Binder.SourceGeneration** node. ## See also