옵션 패턴에서는 옵션의 유효성을 검사하기 위한 다양한 메서드가 표시됩니다. 이러한 메서드에는 데이터 주석 특성을 사용하거나 사용자 지정 유효성 검사기를 사용하는 것이 포함됩니다. 데이터 주석 특성은 런타임에 유효성을 검사하며 성능 비용이 발생할 수 있습니다. 이 문서에서는 옵션 유효성 검사 원본 생성기를 활용하여 컴파일 시간에 최적화된 유효성 검사 코드를 생성하는 방법을 보여 줍니다.
자동 IValidateOptions 구현 생성
옵션 패턴 문서에서는 옵션의 유효성을 검사하기 위해 인터페이스를 IValidateOptions<TOptions> 구현하는 방법을 보여 줍니다. 옵션 유효성 검사 원본 생성기는 옵션 클래스에서 IValidateOptions 데이터 주석 특성을 활용하여 인터페이스 구현을 자동으로 만들 수 있습니다.
다음 콘텐츠는 옵션 패턴 에 표시된 주석 특성 예제를 사용하고 옵션 유효성 검사 원본 생성기를 사용하도록 변환합니다.
다음 appsettings.json 파일을 고려하세요.
{
"MyCustomSettingsSection": {
"SiteTitle": "Amazing docs from awesome people!",
"Scale": 10,
"VerbosityLevel": 32
}
}
다음 클래스는 "MyCustomSettingsSection" 구성 섹션에 바인딩되고 몇 가지 DataAnnotations 규칙을 적용합니다.
using System.ComponentModel.DataAnnotations;
namespace ConsoleJson.Example;
public sealed class SettingsOptions
{
public const string ConfigurationSectionName = "MyCustomSettingsSection";
[Required]
[RegularExpression(@"^[a-zA-Z''!'\s]{1,40}$")]
public required string SiteTitle { get; set; }
[Required]
[Range(0, 1_000,
ErrorMessage = "Value for {0} must be between {1} and {2}.")]
public required int? Scale { get; set; }
[Required]
public required int? VerbosityLevel { get; set; }
}
위의 SettingsOptions 클래스에서 ConfigurationSectionName 속성에는 바인딩할 구성 섹션의 이름이 포함됩니다. 이 시나리오에서 옵션 개체는 구성 섹션의 이름을 제공합니다. 다음 데이터 주석 특성이 사용됩니다.
- RequiredAttribute: 속성이 필요한지 지정합니다.
- RegularExpressionAttribute: 속성 값이 지정된 정규식 패턴과 일치해야 임을 지정합니다.
- RangeAttribute: 속성 값이 지정된 범위 내에 있어야 임을 지정합니다.
팁 (조언)
속성 외에도 RequiredAttribute필수 한정자도 사용합니다. 옵션 객체의 소비자가 속성 값을 설정하는 것을 잊지 않도록 하는 데 도움이 됩니다. 유효성 검사 소스 생성 기능과는 관련이 없습니다.
다음 코드는 구성 섹션을 options 개체에 바인딩하고 데이터 주석의 유효성을 검사하는 방법을 예시합니다.
using ConsoleJson.Example;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddOptions<SettingsOptions>()
.Bind(builder.Configuration.GetSection(SettingsOptions.ConfigurationSectionName));
builder.Services
.AddSingleton<IValidateOptions<SettingsOptions>, ValidateSettingsOptions>();
using IHost app = builder.Build();
var settingsOptions =
app.Services.GetRequiredService<IOptions<SettingsOptions>>().Value;
await app.RunAsync();
팁 (조언)
<PublishAot>true</PublishAot> 파일에 포함하여 AOT 컴파일을 사용하도록 설정하면 코드에서 IL2025 및 IL3050과 같은 경고를 생성할 수 있습니다. 이러한 경고를 완화하려면 구성 원본 생성기를 사용하는 것이 좋습니다. 구성 원본 생성기를 사용하도록 설정하려면 프로젝트 파일에 속성을 <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator> 추가합니다.
옵션 유효성 검사를 위해 컴파일 시간 원본 생성을 활용하여 성능 최적화 유효성 검사 코드를 생성하고 리플렉션이 필요하지 않도록 하여 AOT 호환 앱 빌드를 원활하게 수행할 수 있습니다. 다음 코드에서는 옵션 유효성 검사 원본 생성기를 사용하는 방법을 보여 줍니다.
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
[OptionsValidator]
public partial class ValidateSettingsOptions : IValidateOptions<SettingsOptions>
{
}
빈 부분 클래스의 OptionsValidatorAttribute 존재는 IValidateOptions을(를) 유효성 검사하는 SettingsOptions 인터페이스 구현을 생성하도록 옵션 유효성 검사 원본 생성기에 지시합니다. 옵션 유효성 검사 원본 생성기에서 생성되는 코드는 다음 예제와 유사합니다.
// <auto-generated/>
#nullable enable
#pragma warning disable CS1591 // Compensate for https://github.com/dotnet/roslyn/issues/54103
namespace ConsoleJson.Example
{
partial class ValidateSettingsOptions
{
/// <summary>
/// Validates a specific named options instance (or all when <paramref name="name"/> is <see langword="null" />).
/// </summary>
/// <param name="name">The name of the options instance being validated.</param>
/// <param name="options">The options instance.</param>
/// <returns>Validation result.</returns>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "8.0.9.3103")]
[System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026:RequiresUnreferencedCode",
Justification = "The created ValidationContext object is used in a way that never call reflection")]
public global::Microsoft.Extensions.Options.ValidateOptionsResult Validate(string? name, global::ConsoleJson.Example.SettingsOptions options)
{
global::Microsoft.Extensions.Options.ValidateOptionsResultBuilder? builder = null;
var context = new global::System.ComponentModel.DataAnnotations.ValidationContext(options);
var validationResults = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationResult>();
var validationAttributes = new global::System.Collections.Generic.List<global::System.ComponentModel.DataAnnotations.ValidationAttribute>(2);
context.MemberName = "SiteTitle";
context.DisplayName = string.IsNullOrEmpty(name) ? "SettingsOptions.SiteTitle" : $"{name}.SiteTitle";
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A1);
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A2);
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.SiteTitle, context, validationResults, validationAttributes))
{
(builder ??= new()).AddResults(validationResults);
}
context.MemberName = "Scale";
context.DisplayName = string.IsNullOrEmpty(name) ? "SettingsOptions.Scale" : $"{name}.Scale";
validationResults.Clear();
validationAttributes.Clear();
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A1);
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A3);
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.Scale, context, validationResults, validationAttributes))
{
(builder ??= new()).AddResults(validationResults);
}
context.MemberName = "VerbosityLevel";
context.DisplayName = string.IsNullOrEmpty(name) ? "SettingsOptions.VerbosityLevel" : $"{name}.VerbosityLevel";
validationResults.Clear();
validationAttributes.Clear();
validationAttributes.Add(global::__OptionValidationStaticInstances.__Attributes.A1);
if (!global::System.ComponentModel.DataAnnotations.Validator.TryValidateValue(options.VerbosityLevel, context, validationResults, validationAttributes))
{
(builder ??= new()).AddResults(validationResults);
}
return builder is null ? global::Microsoft.Extensions.Options.ValidateOptionsResult.Success : builder.Build();
}
}
}
namespace __OptionValidationStaticInstances
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "8.0.9.3103")]
file static class __Attributes
{
internal static readonly global::System.ComponentModel.DataAnnotations.RequiredAttribute A1 = new global::System.ComponentModel.DataAnnotations.RequiredAttribute();
internal static readonly global::System.ComponentModel.DataAnnotations.RegularExpressionAttribute A2 = new global::System.ComponentModel.DataAnnotations.RegularExpressionAttribute(
"^[a-zA-Z''-'\\s]{1,40}$");
internal static readonly __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute A3 = new __OptionValidationGeneratedAttributes.__SourceGen__RangeAttribute(
(int)0,
(int)1000)
{
ErrorMessage = "Value for {0} must be between {1} and {2}."
};
}
}
namespace __OptionValidationStaticInstances
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "8.0.9.3103")]
file static class __Validators
{
}
}
namespace __OptionValidationGeneratedAttributes
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Options.SourceGeneration", "8.0.9.3103")]
[global::System.AttributeUsage(global::System.AttributeTargets.Property | global::System.AttributeTargets.Field | global::System.AttributeTargets.Parameter, AllowMultiple = false)]
file class __SourceGen__RangeAttribute : global::System.ComponentModel.DataAnnotations.ValidationAttribute
{
public __SourceGen__RangeAttribute(int minimum, int maximum) : base()
{
Minimum = minimum;
Maximum = maximum;
OperandType = typeof(int);
}
public __SourceGen__RangeAttribute(double minimum, double maximum) : base()
{
Minimum = minimum;
Maximum = maximum;
OperandType = typeof(double);
}
public __SourceGen__RangeAttribute(global::System.Type type, string minimum, string maximum) : base()
{
OperandType = type;
NeedToConvertMinMax = true;
Minimum = minimum;
Maximum = maximum;
}
public object Minimum { get; private set; }
public object Maximum { get; private set; }
public bool MinimumIsExclusive { get; set; }
public bool MaximumIsExclusive { get; set; }
public global::System.Type OperandType { get; }
public bool ParseLimitsInInvariantCulture { get; set; }
public bool ConvertValueInInvariantCulture { get; set; }
public override string FormatErrorMessage(string name) =>
string.Format(global::System.Globalization.CultureInfo.CurrentCulture, GetValidationErrorMessage(), name, Minimum, Maximum);
private bool NeedToConvertMinMax { get; }
private bool Initialized { get; set; }
public override bool IsValid(object? value)
{
if (!Initialized)
{
if (Minimum is null || Maximum is null)
{
throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values.");
}
if (NeedToConvertMinMax)
{
System.Globalization.CultureInfo culture = ParseLimitsInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture;
Minimum = ConvertValue(Minimum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values.");
Maximum = ConvertValue(Maximum, culture) ?? throw new global::System.InvalidOperationException("The minimum and maximum values must be set to valid values.");
}
int cmp = ((global::System.IComparable)Minimum).CompareTo((global::System.IComparable)Maximum);
if (cmp > 0)
{
throw new global::System.InvalidOperationException("The maximum value '{Maximum}' must be greater than or equal to the minimum value '{Minimum}'.");
}
else if (cmp == 0 && (MinimumIsExclusive || MaximumIsExclusive))
{
throw new global::System.InvalidOperationException("Cannot use exclusive bounds when the maximum value is equal to the minimum value.");
}
Initialized = true;
}
if (value is null or string { Length: 0 })
{
return true;
}
System.Globalization.CultureInfo formatProvider = ConvertValueInInvariantCulture ? global::System.Globalization.CultureInfo.InvariantCulture : global::System.Globalization.CultureInfo.CurrentCulture;
object? convertedValue;
try
{
convertedValue = ConvertValue(value, formatProvider);
}
catch (global::System.Exception e) when (e is global::System.FormatException or global::System.InvalidCastException or global::System.NotSupportedException)
{
return false;
}
var min = (global::System.IComparable)Minimum;
var max = (global::System.IComparable)Maximum;
return
(MinimumIsExclusive ? min.CompareTo(convertedValue) < 0 : min.CompareTo(convertedValue) <= 0) &&
(MaximumIsExclusive ? max.CompareTo(convertedValue) > 0 : max.CompareTo(convertedValue) >= 0);
}
private string GetValidationErrorMessage()
{
return (MinimumIsExclusive, MaximumIsExclusive) switch
{
(false, false) => "The field {0} must be between {1} and {2}.",
(true, false) => "The field {0} must be between {1} exclusive and {2}.",
(false, true) => "The field {0} must be between {1} and {2} exclusive.",
(true, true) => "The field {0} must be between {1} exclusive and {2} exclusive.",
};
}
private object? ConvertValue(object? value, System.Globalization.CultureInfo formatProvider)
{
if (value is string stringValue)
{
value = global::System.Convert.ChangeType(stringValue, OperandType, formatProvider);
}
else
{
value = global::System.Convert.ChangeType(value, OperandType, formatProvider);
}
return value;
}
}
}
생성된 코드는 성능에 최적화되어 있으며 리플렉션에 의존하지 않습니다. AOT 호환도 가능합니다. 생성된 코드는 Validators.g.cs 파일에 배치됩니다.
비고
옵션 유효성 검사 원본 생성기를 사용하도록 설정하기 위해 추가 단계를 수행할 필요가 없습니다. 프로젝트에서 Microsoft.Extensions.Options 버전 8 이상을 참조하거나 ASP.NET 애플리케이션을 빌드할 때 기본적으로 자동으로 사용하도록 설정됩니다.
수행해야 하는 유일한 단계는 시작 코드에 다음을 추가하는 것입니다.
builder.Services
.AddSingleton<IValidateOptions<SettingsOptions>, ValidateSettingsOptions>();
비고
옵션 유효성 검사 원본 생성기를 사용하는 경우 호출 OptionsBuilderDataAnnotationsExtensions.ValidateDataAnnotations<TOptions>(OptionsBuilder<TOptions>) 이 필요하지 않습니다.
애플리케이션이 옵션 개체에 액세스하려고 하면 옵션 유효성 검사에 대해 생성된 코드가 실행되어 옵션 개체의 유효성을 검사합니다. 다음 코드 조각은 옵션 개체에 액세스하는 방법을 보여 줍니다.
var settingsOptions =
app.Services.GetRequiredService<IOptions<SettingsOptions>>().Value;
대체된 데이터 주석 속성
생성된 코드를 면밀히 검토하면 처음에 속성RangeAttribute에 적용된 것과 같은 SettingsOptions.Scale원래 데이터 주석 특성이 사용자 지정 특성(예: __SourceGen__RangeAttribute사용자 지정 특성)으로 대체된 것을 확인할 수 있습니다. 이 대체 작업은 리플렉션에 의존하여 유효성 검사를 수행하므로 이루어집니다 RangeAttribute. 반면, __SourceGen__RangeAttribute 성능에 최적화된 사용자 지정 특성이며 리플렉션에 의존하지 않으므로 코드 AOT와 호환됩니다. 특성 대체의 동일한 패턴이 MaxLengthAttribute, MinLengthAttribute, LengthAttribute, 그리고 RangeAttribute에 적용됩니다.
사용자 지정 데이터 주석 특성을 개발하는 모든 사용자의 경우 유효성 검사를 위해 리플렉션을 사용하지 않는 것이 좋습니다. 대신 리플렉션에 의존하지 않는 강력한 형식의 코드를 만드는 것이 좋습니다. 이 방법은 AOT 빌드와의 원활한 호환성을 보장합니다.
참고하십시오
.NET