Dela via


Konfigurationskällgenerator

Från och med .NET 8 introducerades en konfigurationsbindningskällkodgenerator som intercepterar specifika anropsplatser och genererar deras funktionalitet. Den här funktionen tillhandahåller ett Native ahead-of-time (AOT) och trimvänligt sätt att använda konfigurationsbindaren, utan att använda den reflektionsbaserade implementeringen. Reflektion kräver dynamisk kodgenerering, vilket inte stöds i AOT-scenarier.

Den här funktionen är möjlig med tillkomsten av C#-interceptorer som introducerades i C# 12. Med interceptorer kan kompilatorn generera källkod som fångar upp specifika anrop och ersätter dem med genererad kod.

Aktivera konfigurationskällgeneratorn

Om du vill aktivera generatorn för konfigurationskällan lägger du till följande egenskap i projektfilen:

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

När generatorn för konfigurationskällan är aktiverad genererar kompilatorn en källfil som innehåller konfigurationsbindningskoden. Den genererade källan fångar upp bindnings-API:er från följande klasser:

Med andra ord fångas alla API:er som så småningom anropar till dessa olika bindningsmetoder upp och ersätts med genererad kod.

Exempel på användning

Överväg att använda ett .NET-konsolprogram som är konfigurerat för att publicera som en intern AOT-app. Följande kod visar hur du använder konfigurationskällans generator för att binda konfigurationsinställningar:

<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.3" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.3" />
  </ItemGroup>

</Project>

Den föregående projektfilen aktiverar generatorn för konfigurationskällan genom att ställa in EnableConfigurationBindingGenerator egenskapen på true.

Tänk sedan på den Program.cs filen:

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/

Koden ovan:

  • Instansierar en instans av en konfigurationsbyggare.
  • Anropar AddInMemoryCollection och definierar tre konfigurationskällans värden.
  • Anrop Build() för att skapa konfigurationen.
  • ConfigurationBinder.Bind Använder metoden för att binda Settings objektet till konfigurationsvärdena.

När programmet skapas fångar konfigurationskällans generator upp anropet till Bind och genererar bindningskoden.

Viktigt!

När egenskapen PublishAot är inställd på true (eller andra AOT-varningar är aktiverade) och EnabledConfigurationBindingGenerator egenskapen är inställd på false, utlöses varningen IL2026 . Den här varningen indikerar att medlemmar som tillskrivs RequiresUnreferencedCode kan orsaka problem vid trimning. Mer information finns i IL2026.

Utforska den källgenererade koden

Följande kod genereras av konfigurationskällans generator för föregående exempel:

// <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.
    }
}

Anteckning

Den här genererade koden kan komma att ändras baserat på versionen av konfigurationskällans generator.

Den genererade koden innehåller BindingExtensions klassen som innehåller den BindCore metod som utför den faktiska bindningen. Metoden Bind_Settings anropar BindCore metoden och omvandlar instansen till den angivna typen.

Om du vill se den genererade koden anger du <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> i projektfilen. Detta säkerställer att filerna är synliga för utvecklaren för kontroll. Du kan också visa den genererade koden i Visual Studio's Solution Explorer under projektets Dependencies>Analyzers>Microsoft.Extensions.Configuration.Binder.SourceGeneration-nod.

Se även