Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
V modelu možností jsou uvedeny různé metody ověřování možností. Mezi tyto metody patří použití atributů datových poznámek nebo použití vlastního validátoru. Atributy datových poznámek se ověřují za běhu a můžou mít za následek náklady na výkon. Tento článek ukazuje, jak využít možnosti generátoru zdroje ověření k vytvoření optimalizovaného ověřovacího kódu v době kompilace.
Automatické generování implementace IValidateOptions
Článek s vzorem možností ukazuje, jak implementovat IValidateOptions<TOptions> rozhraní pro ověřování možností. Generátor zdrojového kódu pro ověřování nastavení může automaticky vytvořit implementaci rozhraní IValidateOptions pomocí atributů datové anotace ve třídě pro nastavení.
Následující obsah přebírá příklad atributů poznámek zobrazený ve vzoru Možnosti a převede ho na použití generátoru zdrojů ověření možností.
Zvažte následující appsettings.json soubor:
{
"MyCustomSettingsSection": {
"SiteTitle": "Amazing docs from awesome people!",
"Scale": 10,
"VerbosityLevel": 32
}
}
Následující třída vytvoří vazbu na "MyCustomSettingsSection" oddíl konfigurace a použije několik DataAnnotations pravidel:
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; }
}
V předchozí SettingsOptions třídě ConfigurationSectionName obsahuje vlastnost název oddílu konfigurace, ke kterému se má vytvořit vazba. V tomto scénáři objekt options poskytuje název jeho konfiguračního oddílu. Používají se následující atributy datových poznámek:
- RequiredAttribute: Určuje, že vlastnost je povinná.
- RegularExpressionAttribute: Určuje, že hodnota vlastnosti musí odpovídat zadanému vzoru regulárního výrazu.
- RangeAttribute: Určuje, že hodnota vlastnosti musí být v zadaném rozsahu.
Návod
Kromě RequiredAttribute vlastnosti používají také modifikátor požadovaný. To pomáhá zajistit, aby spotřebitelé objektu možností nezapomněli nastavit hodnotu vlastnosti, i když nesouvisí s funkcí generování zdroje ověření.
Následující kód ukazuje, jak vytvořit vazbu konfiguračního oddílu s objektem options a ověřit datové poznámky:
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();
Návod
Pokud je kompilace AOT povolena zahrnutím <PublishAot>true</PublishAot> do souboru .csproj , může kód generovat upozornění, jako je IL2025 a IL3050. Pokud chcete tato upozornění zmírnit, je doporučeno použít generátor zdroje konfigurace. Pokud chcete povolit generátor zdroje konfigurace, přidejte vlastnost <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator> do souboru projektu.
Díky využití generování zdrojů v době kompilace pro ověřování možností můžete vygenerovat ověřovací kód optimalizovaný pro výkon a eliminovat potřebu reflexe, což vede k plynulejšímu sestavování aplikací kompatibilních s AOT. Následující kód ukazuje použití generátoru zdrojů ověřování možností:
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
[OptionsValidator]
public partial class ValidateSettingsOptions : IValidateOptions<SettingsOptions>
{
}
Přítomnost OptionsValidatorAttribute v prázdné částečné třídě dává pokyn generátoru, který ověřuje možnosti, aby vytvořil implementaci rozhraní IValidateOptions, která validuje SettingsOptions. Kód vygenerovaný generátorem zdrojů ověřování možností bude vypadat podobně jako v následujícím příkladu:
// <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;
}
}
}
Vygenerovaný kód je optimalizovaný pro výkon a nespoléhá na reflexi. Je také kompatibilní s AOT. Vygenerovaný kód se umístí do souboru s názvem Validators.g.cs.
Poznámka:
Pokud chcete povolit generátor zdrojů ověřování možností, nemusíte provádět žádné další kroky. Automaticky se povolí, když projekt odkazuje na Microsoft.Extensions.Options verze 8 nebo novější nebo při vytváření ASP.NET aplikace.
Jediným krokem, který je potřeba provést, je přidat do spouštěcího kódu následující:
builder.Services
.AddSingleton<IValidateOptions<SettingsOptions>, ValidateSettingsOptions>();
Poznámka:
Volání OptionsBuilderDataAnnotationsExtensions.ValidateDataAnnotations<TOptions>(OptionsBuilder<TOptions>) se nevyžaduje při použití generátoru zdrojového kódu pro validaci možností.
Když se aplikace pokusí o přístup k objektu možností, vygenerovaný kód pro ověření možností se provede k ověření objektu možností. Následující fragment kódu ukazuje, jak získat přístup k objektu options:
var settingsOptions =
app.Services.GetRequiredService<IOptions<SettingsOptions>>().Value;
Nahrazení atributů datových poznámek
Při úzkém zkoumání vygenerovaného kódu zjistíte, že původní atributy datových poznámek, například RangeAttribute, které byly původně použity na vlastnost SettingsOptions.Scale, byly nahrazeny vlastními atributy jako __SourceGen__RangeAttribute. Tato náhrada se provádí, protože RangeAttribute se spoléhá na reflexi pro ověření. Naproti tomu __SourceGen__RangeAttribute je vlastní atribut optimalizovaný pro výkon a nezávisí na reflexi, takže kód je kompatibilní s AOT. Stejný vzor nahrazení atributu bude použit na MaxLengthAttribute, MinLengthAttributea LengthAttribute kromě RangeAttribute.
Pro každého, kdo vyvíjí vlastní atributy datových poznámek, je vhodné se vyhnout použití reflexe k ověření. Místo toho se doporučuje vytvořit kód silného typu, který se nespoléhá na reflexi. Tento přístup zajišťuje bezproblémovou kompatibilitu se sestaveními AOT.