Skip to content

Commit 2a70c23

Browse files
authored
Add content for config source gen with interceptors (#42994)
* Draft of config source gen with interceptors * Fix attribute * Edit pass * Add a few more links * A bit more specific * More feedback * A few more bits
1 parent 2d97105 commit 2a70c23

File tree

6 files changed

+266
-2
lines changed

6 files changed

+266
-2
lines changed
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
---
2+
title: Compile-time configuration source generation
3+
description: Learn how to use the configuration source generator to intercept specific call sites and bypass reflection-based configuration binding.
4+
author: IEvangelist
5+
ms.author: dapine
6+
ms.date: 10/09/2024
7+
---
8+
9+
# Configuration source generator
10+
11+
Starting with .NET 8, a configuration binding source generator was introduced that intercepts specific call sites and generates their functionality. This feature provides a [Native ahead-of-time (AOT)](../deploying/native-aot/index.md) and [trim-friendly](../deploying/trimming/trim-self-contained.md) way to use the [configuration binder](configuration.md#binding), without the use of the reflection-based implementation. Reflection requires dynamic code generation, which isn't supported in AOT scenarios.
12+
13+
This feature is possible with the advent of [C# interceptors](/dotnet/csharp/whats-new/csharp-12#interceptors) that were introduced in C# 12. Interceptors allow the compiler to generate source code that intercepts specific calls and substitutes them with generated code.
14+
15+
## Enable the configuration source generator
16+
17+
To enable the configuration source generator, add the following property to your project file:
18+
19+
```xml
20+
<PropertyGroup>
21+
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
22+
</PropertyGroup>
23+
```
24+
25+
When the configuration source generator is enabled, the compiler generates a source file that contains the configuration binding code. The generated source intercepts binding APIs from the following classes:
26+
27+
- <xref:Microsoft.Extensions.Configuration.ConfigurationBinder?displayProperty=fullName>
28+
- <xref:Microsoft.Extensions.DependencyInjection.OptionsBuilderConfigurationExtensions?displayProperty=fullName>
29+
- <xref:Microsoft.Extensions.DependencyInjection.OptionsConfigurationServiceCollectionExtensions?displayProperty=nameWithType>
30+
31+
In other words, all APIs that eventually call into these various binding methods are intercepted and replaced with generated code.
32+
33+
## Example usage
34+
35+
Consider a .NET console application configured to publish as a native AOT app. The following code demonstrates how to use the configuration source generator to bind configuration settings:
36+
37+
:::code language="xml" source="snippets/configuration/console-binder-gen/console-binder-gen.csproj" highlight="9,11":::
38+
39+
The preceding project file enables the configuration source generator by setting the `EnableConfigurationBindingGenerator` property to `true`.
40+
41+
Next, consider the _Program.cs_ file:
42+
43+
:::code source="snippets/configuration/console-binder-gen/Program.cs" highlight="12-14":::
44+
45+
The preceding code:
46+
47+
- Instantiates a configuration builder instance.
48+
- Calls <xref:Microsoft.Extensions.Configuration.MemoryConfigurationBuilderExtensions.AddInMemoryCollection%2A> and defines three configuration source values.
49+
- 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.
51+
52+
When the application is built, the configuration source generator intercepts the call to `GetValue<T>` and generates the binding code.
53+
54+
> [!IMPORTANT]
55+
> 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](/dotnet/core/deploying/trimming/trim-warnings/il2026).
56+
57+
### Explore the source generated code
58+
59+
The following code is generated by the configuration source generator for the preceding example:
60+
61+
```csharp
62+
// <auto-generated/>
63+
64+
#nullable enable annotations
65+
#nullable disable warnings
66+
67+
// Suppress warnings about [Obsolete] member usage in generated code.
68+
#pragma warning disable CS0612, CS0618
69+
70+
namespace System.Runtime.CompilerServices
71+
{
72+
using System;
73+
using System.CodeDom.Compiler;
74+
75+
[GeneratedCode(
76+
"Microsoft.Extensions.Configuration.Binder.SourceGeneration",
77+
"8.0.10.31311")]
78+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
79+
file sealed class InterceptsLocationAttribute : Attribute
80+
{
81+
public InterceptsLocationAttribute(string filePath, int line, int column)
82+
{
83+
}
84+
}
85+
}
86+
87+
namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
88+
{
89+
using Microsoft.Extensions.Configuration;
90+
using System;
91+
using System.CodeDom.Compiler;
92+
using System.Globalization;
93+
using System.Runtime.CompilerServices;
94+
95+
[GeneratedCode(
96+
"Microsoft.Extensions.Configuration.Binder.SourceGeneration",
97+
"8.0.10.31311")]
98+
file static class BindingExtensions
99+
{
100+
#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));
109+
#endregion IConfiguration extensions.
110+
111+
#region Core binding extensions.
112+
public static object? GetValueCore(
113+
this IConfiguration configuration, Type type, string key)
114+
{
115+
if (configuration is null)
116+
{
117+
throw new ArgumentNullException(nameof(configuration));
118+
}
119+
120+
IConfigurationSection section = configuration.GetSection(key);
121+
122+
if (section.Value is not string value)
123+
{
124+
return null;
125+
}
126+
127+
if (type == typeof(int))
128+
{
129+
return ParseInt(value, () => section.Path);
130+
}
131+
else if (type == typeof(bool))
132+
{
133+
return ParseBool(value, () => section.Path);
134+
}
135+
else if (type == typeof(global::System.Uri))
136+
{
137+
return ParseSystemUri(value, () => section.Path);
138+
}
139+
140+
return null;
141+
}
142+
143+
public static int ParseInt(
144+
string value, Func<string?> getPath)
145+
{
146+
try
147+
{
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+
}
160+
}
161+
162+
public static bool ParseBool(
163+
string value, Func<string?> getPath)
164+
{
165+
try
166+
{
167+
return bool.Parse(value);
168+
}
169+
catch (Exception exception)
170+
{
171+
throw new InvalidOperationException(
172+
$"Failed to convert configuration value at " +
173+
"'{getPath()}' to type '{typeof(bool)}'.",
174+
exception);
175+
}
176+
}
177+
178+
public static global::System.Uri ParseSystemUri(
179+
string value, Func<string?> getPath)
180+
{
181+
try
182+
{
183+
return new Uri(
184+
value,
185+
UriKind.RelativeOrAbsolute);
186+
}
187+
catch (Exception exception)
188+
{
189+
throw new InvalidOperationException(
190+
$"Failed to convert configuration value at " +
191+
"'{getPath()}' to type '{typeof(global::System.Uri)}'.",
192+
exception);
193+
}
194+
}
195+
#endregion Core binding extensions.
196+
}
197+
}
198+
```
199+
200+
> [!NOTE]
201+
> This generated code is subject to change based on the version of the configuration source generator.
202+
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.
204+
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.
206+
207+
## See also
208+
209+
- [Configuration in .NET](configuration.md)
210+
- [Roslyn: Interceptors feature](https://github.com/dotnet/roslyn/blob/main/docs/features/interceptors.md)
211+
- [Options pattern in .NET](options.md)

docs/core/extensions/configuration.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: Configuration
33
description: Learn how to use the Configuration API to configure .NET applications. Explore various inbuilt configuration providers.
44
author: IEvangelist
55
ms.author: dapine
6-
ms.date: 07/19/2023
6+
ms.date: 10/09/2024
77
ms.topic: overview
88
---
99

@@ -64,7 +64,9 @@ Adding a configuration provider overrides previous configuration values. For exa
6464

6565
### Binding
6666

67-
One of the key advantages of using the .NET configuration abstractions is the ability to bind configuration values to instances of .NET objects. For example, the JSON configuration provider can be used to map *appsettings.json* files to .NET objects and is used with [dependency injection](dependency-injection.md). This enables the [options pattern](options.md), which uses classes to provide strongly typed access to groups of related settings. .NET configuration provides various abstractions. Consider the following interfaces:
67+
One of the key advantages of using the .NET configuration abstractions is the ability to bind configuration values to instances of .NET objects. For example, the JSON configuration provider can be used to map *appsettings.json* files to .NET objects and is used with [dependency injection](dependency-injection.md). This enables the [options pattern](options.md), which uses classes to provide strongly typed access to groups of related settings. The default binder is reflection-based, but there's a [source generator alternative](configuration-generator.md) that's easy to enable.
68+
69+
.NET configuration provides various abstractions. Consider the following interfaces:
6870

6971
- <xref:Microsoft.Extensions.Configuration.IConfiguration>: Represents a set of key/value application configuration properties.
7072
- <xref:Microsoft.Extensions.Configuration.IConfigurationRoot>: Represents the root of an `IConfiguration` hierarchy.

docs/core/extensions/snippets/configuration/configuration.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "options-validation-onstart"
5757
EndProject
5858
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "console-validation-gen", "console-validation-gen\console-validation-gen.csproj", "{D64017C1-B9DD-4FC4-AB1D-62109CF2687C}"
5959
EndProject
60+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "console-binder-gen", "console-binder-gen\console-binder-gen.csproj", "{DCB64163-DF91-4EE8-ABE1-D90AE8DCA54B}"
61+
EndProject
6062
Global
6163
GlobalSection(SolutionConfigurationPlatforms) = preSolution
6264
Debug|Any CPU = Debug|Any CPU
@@ -171,6 +173,10 @@ Global
171173
{D64017C1-B9DD-4FC4-AB1D-62109CF2687C}.Debug|Any CPU.Build.0 = Debug|Any CPU
172174
{D64017C1-B9DD-4FC4-AB1D-62109CF2687C}.Release|Any CPU.ActiveCfg = Release|Any CPU
173175
{D64017C1-B9DD-4FC4-AB1D-62109CF2687C}.Release|Any CPU.Build.0 = Release|Any CPU
176+
{DCB64163-DF91-4EE8-ABE1-D90AE8DCA54B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
177+
{DCB64163-DF91-4EE8-ABE1-D90AE8DCA54B}.Debug|Any CPU.Build.0 = Debug|Any CPU
178+
{DCB64163-DF91-4EE8-ABE1-D90AE8DCA54B}.Release|Any CPU.ActiveCfg = Release|Any CPU
179+
{DCB64163-DF91-4EE8-ABE1-D90AE8DCA54B}.Release|Any CPU.Build.0 = Release|Any CPU
174180
EndGlobalSection
175181
GlobalSection(SolutionProperties) = preSolution
176182
HideSolutionNode = FALSE
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using Microsoft.Extensions.Configuration;
2+
3+
var builder = new ConfigurationBuilder()
4+
.AddInMemoryCollection(initialData: [
5+
new("port", "5001"),
6+
new("enabled", "true"),
7+
new("apiUrl", "https://jsonplaceholder.typicode.com/")
8+
]);
9+
10+
var configuration = builder.Build();
11+
12+
var port = configuration.GetValue<int>("port");
13+
var enabled = configuration.GetValue<bool>("enabled");
14+
var apiUrl = configuration.GetValue<Uri>("apiUrl");
15+
16+
// Write the values to the console.
17+
Console.WriteLine($"Port = {port}");
18+
Console.WriteLine($"Enabled = {enabled}");
19+
Console.WriteLine($"API URL = {apiUrl}");
20+
21+
// This will output the following:
22+
// Port = 5001
23+
// Enabled = True
24+
// API URL = https://jsonplaceholder.typicode.com/
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<RootNamespace>console_binder_gen</RootNamespace>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<Nullable>enable</Nullable>
9+
<PublishAot>true</PublishAot>
10+
<InvariantGlobalization>true</InvariantGlobalization>
11+
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
16+
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
17+
</ItemGroup>
18+
19+
</Project>

docs/fundamentals/toc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1014,6 +1014,8 @@ items:
10141014
- name: Configuration providers
10151015
href: ../core/extensions/configuration-providers.md
10161016
displayName: configuration providers,config providers
1017+
- name: Configuration source generation
1018+
href: ../core/extensions/configuration-generator.md
10171019
- name: Implement a custom configuration provider
10181020
href: ../core/extensions/custom-configuration-provider.md
10191021
displayName: custom configuration,custom config,custom configuration provider,custom config provider

0 commit comments

Comments
 (0)