Configuration source generator

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) and trim-friendly way to use the configuration binder, without the use of the reflection-based implementation. Reflection requires dynamic code generation, which isn't supported in AOT scenarios.

This feature is possible with the advent of C# 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.

Enable the configuration source generator

To enable the configuration source generator, add the following property to your project file:

<PropertyGroup>
    <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>

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:

In other words, all APIs that eventually call into these various binding methods are intercepted and replaced with generated code.

Example usage

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:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <RootNamespace>console_binder_gen</RootNamespace>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <PublishAot>true</PublishAot>
    <InvariantGlobalization>true</InvariantGlobalization>
    <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.2" />
  </ItemGroup>

</Project>

The preceding project file enables the configuration source generator by setting the EnableConfigurationBindingGenerator property to true.

Next, consider the Program.cs file:

using Microsoft.Extensions.Configuration;

var builder = new ConfigurationBuilder()
    .AddInMemoryCollection(initialData: [
            new("port", "5001"),
            new("enabled", "true"),
            new("apiUrl", "https://jsonplaceholder.typicode.com/")
        ]);

var configuration = builder.Build();

var port = configuration.GetValue<int>("port");
var enabled = configuration.GetValue<bool>("enabled");
var apiUrl = configuration.GetValue<Uri>("apiUrl");

// Write the values to the console.
Console.WriteLine($"Port = {port}");
Console.WriteLine($"Enabled = {enabled}");
Console.WriteLine($"API URL = {apiUrl}");

// This will output the following:
//   Port = 5001
//   Enabled = True
//   API URL = https://jsonplaceholder.typicode.com/

The preceding code:

When the application is built, the configuration source generator intercepts the call to GetValue<T> 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 may break when trimming. For more information, see IL2026.

Explore the source generated code

The following code is generated by the configuration source generator for the preceding example:

// <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.10.31311")]
    [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.Globalization;
    using System.Runtime.CompilerServices;

    [GeneratedCode(
        "Microsoft.Extensions.Configuration.Binder.SourceGeneration",
        "8.0.10.31311")]
    file static class BindingExtensions
    {
        #region IConfiguration extensions.
        /// <summary>
        /// Extracts the value with the specified key and converts it to the specified type.
        /// </summary>
        [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<T>(this IConfiguration configuration, string key) =>
            (T?)(BindingExtensions.GetValueCore(configuration, typeof(T), key) ?? default(T));
        #endregion IConfiguration extensions.

        #region Core binding extensions.
        public static object? GetValueCore(
            this IConfiguration configuration, Type type, string key)
        {
            if (configuration is null)
            {
                throw new ArgumentNullException(nameof(configuration));
            }

            IConfigurationSection section = configuration.GetSection(key);

            if (section.Value is not string value)
            {
                return null;
            }

            if (type == typeof(int))
            {
                return ParseInt(value, () => section.Path);
            }
            else if (type == typeof(bool))
            {
                return ParseBool(value, () => section.Path);
            }
            else if (type == typeof(global::System.Uri))
            {
                return ParseSystemUri(value, () => section.Path);
            }

            return null;
        }

        public static int ParseInt(
            string value, Func<string?> getPath)
        {
            try
            {
                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);
            }
        }

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

        public static global::System.Uri ParseSystemUri(
            string value, Func<string?> getPath)
        {
            try
            {
                return new Uri(
                    value,
                    UriKind.RelativeOrAbsolute);
            }
            catch (Exception exception)
            {
                throw new InvalidOperationException(
                    $"Failed to convert configuration value at " +
                    "'{getPath()}' to type '{typeof(global::System.Uri)}'.",
                    exception);
            }
        }
        #endregion Core binding extensions.
    }
}

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<T> extension method calls the GetValueCore method and casts the result to the specified type.

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.

See also