Generatore di origine della configurazione
A partire da .NET 8, è stato introdotto un generatore di origine dell'associazione di configurazione che intercetta siti di chiamata specifici e genera le relative funzionalità. Questa funzionalità offre un metodo AOT (Native ahead-of-time) e trim-friendly per l'uso del binder di configurazione, senza l'uso dell'implementazione basata sulla reflection. La reflection richiede la generazione dinamica del codice, che non è supportata negli scenari AOT.
Questa funzionalità è possibile con l'avvento degli intercettori C# introdotti in C# 12. Gli intercettori consentono al compilatore di generare codice sorgente che intercetta chiamate specifiche e le sostituisce con il codice generato.
Abilitare il generatore di origine della configurazione
Per abilitare il generatore di origine della configurazione, aggiungere la proprietà seguente al file di progetto:
<PropertyGroup>
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>
Quando il generatore di origine della configurazione è abilitato, il compilatore genera un file di origine contenente il codice di associazione di configurazione. L'origine generata intercetta le API di associazione dalle classi seguenti:
- Microsoft.Extensions.Configuration.ConfigurationBinder
- Microsoft.Extensions.DependencyInjection.OptionsBuilderConfigurationExtensions
- Microsoft.Extensions.DependencyInjection.OptionsConfigurationServiceCollectionExtensions
In altre parole, tutte le API che alla fine chiamano in questi vari metodi di associazione vengono intercettate e sostituite con codice generato.
Esempio di utilizzo
Si consideri un'applicazione console .NET configurata per la pubblicazione come app AOT nativa. Il codice seguente illustra come usare il generatore di origine della configurazione per associare le impostazioni di configurazione:
<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="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.1" />
</ItemGroup>
</Project>
Il file di progetto precedente abilita il generatore di origine della configurazione impostando la EnableConfigurationBindingGenerator
proprietà su true
.
Prendere quindi in considerazione il file Program.cs :
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 settings = new Settings();
configuration.Bind(settings);
// Write the values to the console.
Console.WriteLine($"Port = {settings.Port}");
Console.WriteLine($"Enabled = {settings.Enabled}");
Console.WriteLine($"API URL = {settings.ApiUrl}");
class Settings
{
public int Port { get; set; }
public bool Enabled { get; set; }
public string? ApiUrl { get; set; }
}
// This will output the following:
// Port = 5001
// Enabled = True
// API URL = https://jsonplaceholder.typicode.com/
Il codice precedente:
- Crea un'istanza del generatore di configurazione.
- Chiama AddInMemoryCollection e definisce tre valori di origine della configurazione.
- Chiama Build() per compilare la configurazione.
- Usa il ConfigurationBinder.Bind metodo per associare l'oggetto
Settings
ai valori di configurazione.
Quando l'applicazione viene compilata, il generatore di origine della configurazione intercetta la chiamata a Bind
e genera il codice di associazione.
Importante
Quando la PublishAot
proprietà è impostata su true
(o sono abilitati altri avvisi AOT) e la EnabledConfigurationBindingGenerator
proprietà è impostata su false
, viene generato un avviso IL2026
. Questo avviso indica che i membri vengono attribuiti con RequiresUnreferencedCode possono interrompersi quando si esegue il taglio. Per altre informazioni, vedere IL2026.
Esplorare il codice generato dall'origine
Il codice seguente viene generato dal generatore di origine della configurazione per l'esempio precedente:
// <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", "9.0.10.47305")]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute : Attribute
{
public InterceptsLocationAttribute(int version, string data)
{
}
}
}
namespace Microsoft.Extensions.Configuration.Binder.SourceGeneration
{
using Microsoft.Extensions.Configuration;
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.CompilerServices;
[GeneratedCode("Microsoft.Extensions.Configuration.Binder.SourceGeneration", "9.0.10.47305")]
file static class BindingExtensions
{
#region IConfiguration extensions.
/// <summary>Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively.</summary>
[InterceptsLocation(1, "uDIs2gDFz/yEvxOzjNK4jnIBAABQcm9ncmFtLmNz")] // D:\source\WorkerService1\WorkerService1\Program.cs(13,15)
public static void Bind_Settings(this IConfiguration configuration, object? instance)
{
ArgumentNullException.ThrowIfNull(configuration);
if (instance is null)
{
return;
}
var typedObj = (global::Settings)instance;
BindCore(configuration, ref typedObj, defaultValueIfNotFound: false, binderOptions: null);
}
#endregion IConfiguration extensions.
#region Core binding extensions.
private readonly static Lazy<HashSet<string>> s_configKeys_Settings = new(() => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "Port", "Enabled", "ApiUrl" });
public static void BindCore(IConfiguration configuration, ref global::Settings instance, bool defaultValueIfNotFound, BinderOptions? binderOptions)
{
ValidateConfigurationKeys(typeof(global::Settings), s_configKeys_Settings, configuration, binderOptions);
if (configuration["Port"] is string value0 && !string.IsNullOrEmpty(value0))
{
instance.Port = ParseInt(value0, configuration.GetSection("Port").Path);
}
else if (defaultValueIfNotFound)
{
instance.Port = instance.Port;
}
if (configuration["Enabled"] is string value1 && !string.IsNullOrEmpty(value1))
{
instance.Enabled = ParseBool(value1, configuration.GetSection("Enabled").Path);
}
else if (defaultValueIfNotFound)
{
instance.Enabled = instance.Enabled;
}
if (configuration["ApiUrl"] is string value2)
{
instance.ApiUrl = value2;
}
else if (defaultValueIfNotFound)
{
var currentValue = instance.ApiUrl;
if (currentValue is not null)
{
instance.ApiUrl = currentValue;
}
}
}
/// <summary>If required by the binder options, validates that there are no unknown keys in the input configuration object.</summary>
public static void ValidateConfigurationKeys(Type type, Lazy<HashSet<string>> keys, IConfiguration configuration, BinderOptions? binderOptions)
{
if (binderOptions?.ErrorOnUnknownConfiguration is true)
{
List<string>? temp = null;
foreach (IConfigurationSection section in configuration.GetChildren())
{
if (!keys.Value.Contains(section.Key))
{
(temp ??= new List<string>()).Add($"'{section.Key}'");
}
}
if (temp is not null)
{
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)}");
}
}
}
public static int ParseInt(string value, string? path)
{
try
{
return int.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);
}
catch (Exception exception)
{
throw new InvalidOperationException($"Failed to convert configuration value at '{path}' to type '{typeof(int)}'.", exception);
}
}
public static bool ParseBool(string value, string? path)
{
try
{
return bool.Parse(value);
}
catch (Exception exception)
{
throw new InvalidOperationException($"Failed to convert configuration value at '{path}' to type '{typeof(bool)}'.", exception);
}
}
#endregion Core binding extensions.
}
}
Nota
Questo codice generato è soggetto a modifiche in base alla versione del generatore di origine della configurazione.
Il codice generato contiene la BindingExtensions
classe , che contiene il BindCore
metodo che esegue l'associazione effettiva. Il Bind_Settings
metodo chiama il BindCore
metodo ed esegue il cast dell'istanza al tipo specificato.
Per visualizzare il codice generato, impostare <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
nel file di progetto. In questo modo si garantisce che i file siano visibili allo sviluppatore per l'ispezione. È anche possibile visualizzare il codice generato nel Esplora soluzioni di Visual Studio nel nodo Dependencies>Analyzers>Microsoft.Extensions.Configuration.Binder.SourceGeneration del progetto.