Skip to content

Commit 6d02d4a

Browse files
authored
Update configuration source generator documentation to use a class to bind the config values. (#43388)
1 parent d288afa commit 6d02d4a

File tree

2 files changed

+83
-73
lines changed

2 files changed

+83
-73
lines changed

docs/core/extensions/configuration-generator.md

Lines changed: 71 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,16 @@ The preceding project file enables the configuration source generator by setting
4040

4141
Next, consider the _Program.cs_ file:
4242

43-
:::code source="snippets/configuration/console-binder-gen/Program.cs" highlight="12-14":::
43+
:::code source="snippets/configuration/console-binder-gen/Program.cs" highlight="13":::
4444

4545
The preceding code:
4646

4747
- Instantiates a configuration builder instance.
4848
- Calls <xref:Microsoft.Extensions.Configuration.MemoryConfigurationBuilderExtensions.AddInMemoryCollection%2A> and defines three configuration source values.
4949
- Calls <xref:Microsoft.Extensions.Configuration.IConfigurationBuilder.Build> to build the configuration.
50-
- Uses the <xref:Microsoft.Extensions.Configuration.ConfigurationBinder.GetValue%2A?displayProperty=nameWithType> extension method to get the value for each configuration key.
50+
- Uses the <xref:Microsoft.Extensions.Configuration.ConfigurationBinder.Bind%2A?displayProperty=nameWithType> method to bind the `Settings` object to the configuration values.
5151

52-
When the application is built, the configuration source generator intercepts the call to `GetValue<T>` and generates the binding code.
52+
When the application is built, the configuration source generator intercepts the call to `Bind` and generates the binding code.
5353

5454
> [!IMPORTANT]
5555
> 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
7272
using System;
7373
using System.CodeDom.Compiler;
7474

75-
[GeneratedCode(
76-
"Microsoft.Extensions.Configuration.Binder.SourceGeneration",
77-
"8.0.10.31311")]
75+
[GeneratedCode("Microsoft.Extensions.Configuration.Binder.SourceGeneration", "9.0.10.47305")]
7876
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
7977
file sealed class InterceptsLocationAttribute : Attribute
8078
{
81-
public InterceptsLocationAttribute(string filePath, int line, int column)
79+
public InterceptsLocationAttribute(int version, string data)
8280
{
8381
}
8482
}
@@ -89,107 +87,113 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
8987
using Microsoft.Extensions.Configuration;
9088
using System;
9189
using System.CodeDom.Compiler;
90+
using System.Collections.Generic;
9291
using System.Globalization;
9392
using System.Runtime.CompilerServices;
9493

95-
[GeneratedCode(
96-
"Microsoft.Extensions.Configuration.Binder.SourceGeneration",
97-
"8.0.10.31311")]
94+
[GeneratedCode("Microsoft.Extensions.Configuration.Binder.SourceGeneration", "9.0.10.47305")]
9895
file static class BindingExtensions
9996
{
10097
#region IConfiguration extensions.
101-
/// <summary>
102-
/// Extracts the value with the specified key and converts it to the specified type.
103-
/// </summary>
104-
[InterceptsLocation(@"C:\source\configuration\console-binder-gen\Program.cs", 12, 26)]
105-
[InterceptsLocation(@"C:\source\configuration\console-binder-gen\Program.cs", 13, 29)]
106-
[InterceptsLocation(@"C:\source\configuration\console-binder-gen\Program.cs", 14, 28)]
107-
public static T? GetValue<T>(this IConfiguration configuration, string key) =>
108-
(T?)(BindingExtensions.GetValueCore(configuration, typeof(T), key) ?? default(T));
98+
/// <summary>Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively.</summary>
99+
[InterceptsLocation(1, "uDIs2gDFz/yEvxOzjNK4jnIBAABQcm9ncmFtLmNz")] // D:\source\WorkerService1\WorkerService1\Program.cs(13,15)
100+
public static void Bind_Settings(this IConfiguration configuration, object? instance)
101+
{
102+
ArgumentNullException.ThrowIfNull(configuration);
103+
104+
if (instance is null)
105+
{
106+
return;
107+
}
108+
109+
var typedObj = (global::Settings)instance;
110+
BindCore(configuration, ref typedObj, defaultValueIfNotFound: false, binderOptions: null);
111+
}
109112
#endregion IConfiguration extensions.
110113

111114
#region Core binding extensions.
112-
public static object? GetValueCore(
113-
this IConfiguration configuration, Type type, string key)
115+
private readonly static Lazy<HashSet<string>> s_configKeys_Settings = new(() => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Port", "Enabled", "ApiUrl" });
116+
117+
public static void BindCore(IConfiguration configuration, ref global::Settings instance, bool defaultValueIfNotFound, BinderOptions? binderOptions)
114118
{
115-
if (configuration is null)
119+
ValidateConfigurationKeys(typeof(global::Settings), s_configKeys_Settings, configuration, binderOptions);
120+
121+
if (configuration["Port"] is string value0 && !string.IsNullOrEmpty(value0))
116122
{
117-
throw new ArgumentNullException(nameof(configuration));
123+
instance.Port = ParseInt(value0, configuration.GetSection("Port").Path);
118124
}
119-
120-
IConfigurationSection section = configuration.GetSection(key);
121-
122-
if (section.Value is not string value)
125+
else if (defaultValueIfNotFound)
123126
{
124-
return null;
127+
instance.Port = instance.Port;
125128
}
126129

127-
if (type == typeof(int))
130+
if (configuration["Enabled"] is string value1 && !string.IsNullOrEmpty(value1))
128131
{
129-
return ParseInt(value, () => section.Path);
132+
instance.Enabled = ParseBool(value1, configuration.GetSection("Enabled").Path);
130133
}
131-
else if (type == typeof(bool))
134+
else if (defaultValueIfNotFound)
132135
{
133-
return ParseBool(value, () => section.Path);
136+
instance.Enabled = instance.Enabled;
134137
}
135-
else if (type == typeof(global::System.Uri))
138+
139+
if (configuration["ApiUrl"] is string value2)
136140
{
137-
return ParseSystemUri(value, () => section.Path);
141+
instance.ApiUrl = value2;
142+
}
143+
else if (defaultValueIfNotFound)
144+
{
145+
var currentValue = instance.ApiUrl;
146+
if (currentValue is not null)
147+
{
148+
instance.ApiUrl = currentValue;
149+
}
138150
}
139-
140-
return null;
141151
}
142152

143-
public static int ParseInt(
144-
string value, Func<string?> getPath)
153+
154+
/// <summary>If required by the binder options, validates that there are no unknown keys in the input configuration object.</summary>
155+
public static void ValidateConfigurationKeys(Type type, Lazy<HashSet<string>> keys, IConfiguration configuration, BinderOptions? binderOptions)
145156
{
146-
try
157+
if (binderOptions?.ErrorOnUnknownConfiguration is true)
147158
{
148-
return int.Parse(
149-
value,
150-
NumberStyles.Integer,
151-
CultureInfo.InvariantCulture);
152-
}
153-
catch (Exception exception)
154-
{
155-
throw new InvalidOperationException(
156-
$"Failed to convert configuration value at " +
157-
"'{getPath()}' to type '{typeof(int)}'.",
158-
exception);
159+
List<string>? temp = null;
160+
161+
foreach (IConfigurationSection section in configuration.GetChildren())
162+
{
163+
if (!keys.Value.Contains(section.Key))
164+
{
165+
(temp ??= new List<string>()).Add($"'{section.Key}'");
166+
}
167+
}
168+
169+
if (temp is not null)
170+
{
171+
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)}");
172+
}
159173
}
160174
}
161175

162-
public static bool ParseBool(
163-
string value, Func<string?> getPath)
176+
public static int ParseInt(string value, string? path)
164177
{
165178
try
166179
{
167-
return bool.Parse(value);
180+
return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
168181
}
169182
catch (Exception exception)
170183
{
171-
throw new InvalidOperationException(
172-
$"Failed to convert configuration value at " +
173-
"'{getPath()}' to type '{typeof(bool)}'.",
174-
exception);
184+
throw new InvalidOperationException($"Failed to convert configuration value at '{path}' to type '{typeof(int)}'.", exception);
175185
}
176186
}
177187

178-
public static global::System.Uri ParseSystemUri(
179-
string value, Func<string?> getPath)
188+
public static bool ParseBool(string value, string? path)
180189
{
181190
try
182191
{
183-
return new Uri(
184-
value,
185-
UriKind.RelativeOrAbsolute);
192+
return bool.Parse(value);
186193
}
187194
catch (Exception exception)
188195
{
189-
throw new InvalidOperationException(
190-
$"Failed to convert configuration value at " +
191-
"'{getPath()}' to type '{typeof(global::System.Uri)}'.",
192-
exception);
196+
throw new InvalidOperationException($"Failed to convert configuration value at '{path}' to type '{typeof(bool)}'.", exception);
193197
}
194198
}
195199
#endregion Core binding extensions.
@@ -200,9 +204,9 @@ namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
200204
> [!NOTE]
201205
> This generated code is subject to change based on the version of the configuration source generator.
202206
203-
The generated code contains the `BindingExtensions` class, which contains the `GetValueCore` method that performs the actual binding. The `GetValue<T>` extension method calls the `GetValueCore` method and casts the result to the specified type.
207+
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.
204208

205-
To see the generated code, set the `<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>` in the project file. This ensures that the files are visible to the developer for inspection.
209+
To see the generated code, set the `<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>` 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.
206210

207211
## See also
208212

docs/core/extensions/snippets/configuration/console-binder-gen/Program.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,20 @@
99

1010
var configuration = builder.Build();
1111

12-
var port = configuration.GetValue<int>("port");
13-
var enabled = configuration.GetValue<bool>("enabled");
14-
var apiUrl = configuration.GetValue<Uri>("apiUrl");
12+
var settings = new Settings();
13+
configuration.Bind(settings);
1514

1615
// Write the values to the console.
17-
Console.WriteLine($"Port = {port}");
18-
Console.WriteLine($"Enabled = {enabled}");
19-
Console.WriteLine($"API URL = {apiUrl}");
16+
Console.WriteLine($"Port = {settings.Port}");
17+
Console.WriteLine($"Enabled = {settings.Enabled}");
18+
Console.WriteLine($"API URL = {settings.ApiUrl}");
19+
20+
class Settings
21+
{
22+
public int Port { get; set; }
23+
public bool Enabled { get; set; }
24+
public string? ApiUrl { get; set; }
25+
}
2026

2127
// This will output the following:
2228
// Port = 5001

0 commit comments

Comments
 (0)