구성 원본 생성기
.NET 8부터 특정 호출 사이트를 가로채 해당 기능을 생성하는 구성 바인딩 원본 생성기가 도입되었습니다. 이 기능은 리플렉션 기반 구현을 사용하지 않고도 네이티브 AOT(Ahead-Of-Time) 및 트리밍 친화적인 구성 바인더를 사용하는 방법을 제공합니다. 리플렉션에는 AOT 시나리오에서 지원되지 않는 동적 코드 생성이 필요합니다.
이 기능은 C# 12에서 도입된 C# 인터셉터가 등장하면서 가능합니다. 인터셉터를 사용하면 컴파일러가 특정 호출을 가로채 생성된 코드로 대체하는 소스 코드를 생성할 수 있습니다.
구성 원본 생성기 사용
구성 원본 생성기를 사용하도록 설정하려면 프로젝트 파일에 다음 속성을 추가합니다.
<PropertyGroup>
<EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
</PropertyGroup>
구성 원본 생성기를 사용하도록 설정하면 컴파일러는 구성 바인딩 코드를 포함하는 소스 파일을 생성합니다. 생성된 원본은 다음 클래스에서 바인딩 API를 가로채는 데 사용됩니다.
- Microsoft.Extensions.Configuration.ConfigurationBinder
- Microsoft.Extensions.DependencyInjection.OptionsBuilderConfigurationExtensions
- Microsoft.Extensions.DependencyInjection.OptionsConfigurationServiceCollectionExtensions
즉, 결국 이러한 다양한 바인딩 메서드를 호출하는 모든 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.3" />
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.3" />
</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 하고 정의합니다.
- 구성을 빌드하기 위한 호출 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.
}
}
참고
이 생성된 코드는 구성 원본 생성기의 버전에 따라 변경될 수 있습니다.
생성된 코드는 BindingExtensions
클래스를 포함하며, 이 클래스에는 실제 바인딩을 수행하는 BindCore
메서드가 포함되어 있습니다. 메서드는 Bind_Settings
메서드를 BindCore
호출하고 인스턴스를 지정된 형식으로 캐스팅합니다.
생성된 코드를 보려면 프로젝트 파일에서 설정합니다 <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
. 이렇게 하면 개발자가 검사를 위해 파일을 볼 수 있습니다. Visual Studio의 솔루션 탐색기 프로젝트의 종속성>
참조
.NET