Skip to content

Developers can safely trim apps which need Configuration Binder #44493

@eerhardt

Description

@eerhardt

Edited by @layomia.

Background

Application configuration in ASP.NET Core is performed using one or more configuration providers. Configuration providers read data (as key-value pairs) using a variety of sources such as settings files (e.g. appsettings.json), environment variables, Azure Key Vault etc.

At the core of this mechanism is ConfigurationBinder, an extension class in platform extensions that provides Bind and Get methods that maps configuration values (IConfiguration instances) to strongly-typed objects. Bind takes an instance, while Get creates one on behalf of the caller. The current approach currently uses reflection which causes issues for trimming and Native AOT.

Here's example of code that users write to invoke the binder:

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

// ASP.NET Core apps configure and launch a host, responsible for app startup and lifetime management.
// Templates create a WebApplicationBuilder which contains the host and provides default configuration
// for the app that may be specified by users through the mechanisms shown above.
// `args` contains configuration values specified through command line
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// Represents a section of application configuration values (key-value pairs).
// Implements `IConfiguration`.
IConfigurationSection section = builder.Configuration.GetSection("MyOptions");

// !! Configure call - to be replaced with source-gen'd implementation
// `builder.Services`: a collection of user or framework-provided services for the application to compose
// `Configure<MyOptions>(section)`: adds the configuration section to the service collection for use in their operations
// `MyOptions`: configuration shape/type that maps to command-line specified config values
builder.Services.Configure<MyOptions>(section);

// !! Get call - to be replaced with source-gen'd implementation
// Get a new instance of your options using values from the config section
MyOptions options0 = section.Get<MyOptions>();

// Bind config options to an existing instance
MyOptions options1 = new MyOptions();
section.Bind(myOptions1);

// Build the application.
WebApplication app = builder.Build();
// Specify a http GET operation
app.MapGet("/", () => "Hello World!");
// Run the app
app.Run();

// Configuration classes mapping to user-provided values
public class MyOptions
{
    public int A { get; set; }
    public string S { get; set; }
    public byte[] Data { get; set; }
    public Dictionary<string, string> Values { get; set; }
    public List<MyClass> Values2 { get; set; }
}

public class MyClass
{
    public int SomethingElse { get; set; }
}

Configuration binding source generator in .NET 8

In .NET 8, as a replacement to reflection, we wish to ship a source generator that generates static code to bind config values to target user types. We are proposing an implicit mechanism where we probe for Configure, Bind, and Get calls that we can retrieve type info from.

Given the sample user code above, here's the code that would be generated:

using System.Collections.Generic;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensison.DependencyInjection;

public static class GeneratedConfigurationBinder
{
    public static IServiceCollection Configure<T>(this IServiceCollection services, IConfiguration configuration)
    {
        if (typeof(T) == typeof(MyOptions))
        {
            // Redirect to generated Bind method.
            return services.Configure<MyOptions>(obj => Bind(configuration, obj));
        }

        // Repeat if-check for all input types.

        throw new NotSupportedException("The source generator did not detect this type as input.");
    }

    public static T? Get<T>(this global::Microsoft.Extensions.Configuration.IConfiguration configuration)
    {
        if (typeof(T) == typeof(MyOptions))
        {
            MyOptions obj = new();
            Bind(configuration, obj);
            return (T)(object)obj;
        }

        // Repeat if-check for all input types.

        throw new NotSupportedException("The source generator did not detect this type as input.");
    }

    internal static void Bind(this IConfiguration configuration, MyOptions obj)
    {
        if (obj is null)
        {
            throw new ArgumentNullException(nameof(obj));
        }
        
        // Preliminary code to validate input configuration.

        obj.A = configuration["A"] is String stringValue0 ? int.Parse(stringValue4) : default;
        obj.S = configuration["S"] is String stringValue1 ? bool.Parse(stringValue5) : default;
        obj.Values ??= new();
        Bind(configuration.GetSection("Values"), obj.Values);
        obj.Values2 ?? = new();
        Bind(configuration.GetSection("Values2"), obj.Values2);
    }

    private static void Bind(this IConfiguration configuration, Dictionary<string, string> obj)
    {
        if (obj is null)
        {
            throw new global::System.ArgumentNullException(nameof(obj));
        }

        string key;
        string element;

        foreach (Microsoft.Extensions.Configuration.IConfigurationSection section in configuration.GetChildren())
        {
            key = section["Key"] is string stringValue2 ? stringVal2 : default;
            element = section["Value"] is string stringValue3 ? stringValue3 : default;
            obj[key] = element;
        }
    }

    private static void Bind(this IConfiguration configuration, List<MyClass> obj)
    {
        throw new NotSupportedException("This type is not supported.");
    }
}

Generator kick-off gesture

We want the user experience to be easy and minimal. Starting off, the generator will simply probe for a compiler visible property called UseConfigurationBinderSourceGenerator to determine whether to run. This allows folks that don't want the generator or for which it doesn't work for (yet) to skip using it.

<ItemGroup>
    <CompilerVisibleProperty Include="UseConfigurationBinderSourceGenerator" />
</ItemGroup>

We'll add this as an SDK property later.

Generation strategy:

The operation is to convert IConfiguration instances into target config types. We'll do this by generate bespoke parsing/mapping logic for each target type. This is similar to a "fast-path" deserialization feature that the System.Text.Json source generator could implement (#55043).

PR: #82179

Integration with user apps

The code is generated into the global namespace and the methods are picked over the reflection by the compiler.

  • As a next step we want to use the call site replace feature being developed by Roslyn to replace user calls with generated calls directly.

Technical details

  • This approach satisfies the direct-call scenarios in user apps. As a next step we need to design a solution for indirect usage, e.g. calls the ASP.NET makes on behalf of users. A design isn't fully laid out yet but early ideas are to have some sort of global cache from implicit registration of generated code.

What's next

#79527


Related docs

Initial design

Metadata

Metadata

Assignees

Labels

Priority:2Work that is important, but not critical for the releaseTeam:LibrariesUser StoryA single user-facing feature. Can be grouped under an epic.api-needs-workAPI needs work before it is approved, it is NOT ready for implementationarea-Extensions-Configuration

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions