次の方法で共有


構成ソース ジェネレーター

.NET 8 以降では、特定の呼び出しサイトをインターセプトし、その機能を生成する構成バインディング ソース ジェネレーターが導入されました。 この機能は、リフレクション ベースの実装を使用せずに、事前 (AOT) trim フレンドリ構成バインダーを使用する方法を提供します。 リフレクションには、AOT シナリオではサポートされていない動的なコード生成が必要です。

この機能は、C# 12 で導入された C# インターセプター が登場した場合に可能です。 インターセプターを使用すると、コンパイラは特定の呼び出しをインターセプトし、生成されたコードに置き換えるソース コードを生成できます。

構成ソース ジェネレーターを有効にする

構成ソース ジェネレーターを有効にするには、プロジェクト ファイルに次のプロパティを追加します。

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

構成ソース ジェネレーターが有効になっている場合、コンパイラは、構成バインディング コードを含むソース ファイルを生成します。 生成されたソースは、次のクラスからバインディング API をインターセプトします。

つまり、これらのさまざまなバインド メソッドを最終的に呼び出すすべての API がインターセプトされ、生成されたコードに置き換えられます。

使用例

ネイティブ AOT アプリとして発行するように構成された .NET コンソール アプリケーションについて考えてみましょう。 次のコードは、構成ソース ジェネレーターを使用して構成設定をバインドする方法を示しています。

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

</Project>

上記のプロジェクト ファイルでは、 EnableConfigurationBindingGenerator プロパティを trueに設定することで、構成ソース ジェネレーターを有効にします。

次に、 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/

上記のコードでは次の操作が行われます。

  • 構成ビルダー インスタンスをインスタンス化します。
  • AddInMemoryCollectionを呼び出し、3 つの構成ソース値を定義します。
  • Build()を呼び出して構成をビルドします。
  • ConfigurationBinder.Bind メソッドを使用して、Settings オブジェクトを構成値にバインドします。

アプリケーションがビルドされると、構成ソース ジェネレーターは、 Bind の呼び出しをインターセプトし、バインド コードを生成します。

重要

PublishAot プロパティがtrueに設定され (または他の AOT 警告が有効になっている)、EnabledConfigurationBindingGenerator プロパティがfalseに設定されている場合、警告IL2026が発生します。 この警告は、メンバーが RequiresUnreferencedCode トリミング時に中断する可能性があることを示します。 詳細については、「 IL2026」を参照してください。

ソースで生成されたコードを調べる

次のコードは、前の例の構成ソース ジェネレーターによって生成されます。

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

Note

この生成されたコードは、構成ソース ジェネレーターのバージョンに基づいて変更される可能性があります。

生成されたコードには、実際のバインドを実行するBindCore メソッドを含むBindingExtensions クラスが含まれています。 Bind_Settings メソッドは、BindCore メソッドを呼び出し、指定した型にインスタンスをキャストします。

生成されたコードを表示するには、プロジェクト ファイルで <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> を設定します。 これにより、ファイルが検査のために開発者に表示されるようになります。 生成されたコードは、プロジェクトの Dependencies>Analyzers>Microsoft.Extensions.Configuration.Binder.SourceGeneration ノードの下にある Visual Studio のソリューション エクスプローラーで表示することもできます。

関連項目