-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Description
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.