Jegyzet
Az oldalhoz való hozzáférés engedélyezést igényel. Próbálhatod be jelentkezni vagy könyvtárat váltani.
Az oldalhoz való hozzáférés engedélyezést igényel. Megpróbálhatod a könyvtár váltását.
A beállítási mintában különböző érvényesítési módszerek jelennek meg. Ezek a módszerek magukban foglalják az adatjegyzet-attribútumok használatát vagy egy egyéni érvényesítő használatát. Az adatjegyzet-attribútumok futásidőben vannak érvényesítve, és teljesítményköltségeket okozhatnak. Ez a cikk bemutatja, hogyan használható a beállítások érvényesítési forrásgenerátora az optimalizált érvényesítési kód fordításkor történő előállítására.
Automatikus IValidateOptions-implementáció létrehozása
A beállításminta-cikk bemutatja, hogyan implementálhatja a kezelőfelületet a IValidateOptions<TOptions> beállítások érvényesítéséhez. A beállításérvényesítési forrásgenerátor automatikusan létrehozhatja az illesztő implementációt a IValidateOptions beállításosztály adatjegyzet-attribútumainak használatával.
Az alábbi tartalom a Beállítások mintában látható jegyzetattribútumok példáját követi, és átalakítja a beállítások érvényesítési forrásgenerátorának használatára.
Vegye figyelembe a következő appsettings.json fájlt:
{
"MyCustomSettingsSection": {
"SiteTitle": "Amazing docs from awesome people!",
"Scale": 10,
"VerbosityLevel": 32
}
}
A következő osztály a "MyCustomSettingsSection" konfigurációs szakaszhoz kötődik, és néhány DataAnnotations szabályt alkalmaz:
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; }
}
Az előző SettingsOptions osztályban a ConfigurationSectionName tulajdonság tartalmazza annak a konfigurációs szakasznak a nevét, amelyhez csatlakozni szeretne. Ebben a forgatókönyvben a beállításobjektum adja meg a konfigurációs szakasz nevét. A rendszer a következő adatjegyzet-attribútumokat használja:
- RequiredAttribute: Megadja, hogy a tulajdonság szükséges-e.
- RegularExpressionAttribute: Azt adja meg, hogy a tulajdonság értékének meg kell egyeznie a megadott reguláris kifejezésmintával.
- RangeAttribute: Azt határozza meg, hogy a tulajdonságértéknek egy megadott tartományon belül kell lennie.
Jótanács
A tulajdonságok a RequiredAttribute mellett a szükséges módosítót is használják. Ez segít biztosítani, hogy a beállítási objektum felhasználói ne felejtsék el beállítani a tulajdonságértéket, bár nem kapcsolódnak az érvényesítési forrásgenerálási funkcióhoz.
Az alábbi kód bemutatja, hogyan kötheti a konfigurációs szakaszt a beállításobjektumhoz, és hogyan ellenőrizheti az adatjegyzeteket:
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();
Jótanács
Ha az AOT-fordítás engedélyezve van a <PublishAot>true</PublishAot> fájlba való belefoglalással, a kód figyelmeztetéseket generálhat, például IL2025 és IL3050. A figyelmeztetések enyhítése érdekében ajánlott a konfigurációs forrásgenerátort használni. A konfigurációs forrásgenerátor engedélyezéséhez adja hozzá a tulajdonságot <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator> a projektfájlhoz.
A fordítási idő forrásának a beállítások érvényesítéséhez való használatával teljesítményoptimalizált érvényesítési kódot hozhat létre, és szükségtelenné teheti a tükrözést, ami gördülékenyebb AOT-kompatibilis alkalmazásépítést eredményez. Az alábbi kód bemutatja, hogyan használható a beállítások érvényesítési forrásgenerátora:
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
[OptionsValidator]
public partial class ValidateSettingsOptions : IValidateOptions<SettingsOptions>
{
}
Az üres részleges osztály jelenléte OptionsValidatorAttribute azt utasítja az opciók érvényesítését végző forrásgenerátort, hogy létrehozza az IValidateOptions interfész implementációját, amely érvényesíti SettingsOptions. A beállítások érvényesítési forrásgenerátora által létrehozott kód a következő példához fog hasonlítni:
// <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;
}
}
}
A létrehozott kód a teljesítményre van optimalizálva, és nem támaszkodik a tükröződésre. Emellett AOT-kompatibilis is. A létrehozott kód egy Validators.g.cs nevű fájlba kerül.
Megjegyzés:
A beállítások érvényesítési forrásgenerátorának engedélyezéséhez nem kell további lépéseket tennie. Alapértelmezés szerint automatikusan engedélyezve van, ha a projekt a Microsoft.Extensions.Options 8-os vagy újabb verziójára hivatkozik, vagy ASP.NET alkalmazás létrehozásakor.
Az egyetlen lépés, amit meg kell tennie, hogy hozzáadja a következőket az indítási kódhoz:
builder.Services
.AddSingleton<IValidateOptions<SettingsOptions>, ValidateSettingsOptions>();
Megjegyzés:
A beállításérvényesítési forrásgenerátor használatakor nincs szükség hívásra OptionsBuilderDataAnnotationsExtensions.ValidateDataAnnotations<TOptions>(OptionsBuilder<TOptions>) .
Amikor az alkalmazás megpróbálja elérni a beállításobjektumot, a rendszer végrehajtja a beállítások érvényesítéséhez létrehozott kódot a beállításobjektum ellenőrzéséhez. Az alábbi kódrészlet bemutatja, hogyan érheti el a beállításobjektumot:
var settingsOptions =
app.Services.GetRequiredService<IOptions<SettingsOptions>>().Value;
Adatjegyzet-attribútumok lecserélése
A generált kód alapos vizsgálata után megfigyelheti, hogy az eredeti adatjegyzet-attribútumok, például RangeAttributeaz eredetileg a tulajdonságra SettingsOptions.Scalealkalmazott attribútumok olyan egyéni attribútumokkal lettek helyettesítve, mint a __SourceGen__RangeAttribute. Ez a helyettesítés azért történik, mert a RangeAttribute visszatükrözésre támaszkodik az ellenőrzéshez. Ezzel szemben egy teljesítményre optimalizált egyéni attribútum, __SourceGen__RangeAttribute amely nem függ a tükröződéstől, így a kód AOT-kompatibilis. Ugyanez az attribútum-helyettesítési minta lesz alkalmazva az MaxLengthAttribute, MinLengthAttribute, és LengthAttribute az RangeAttribute-en kívül is.
Bárki számára, aki egyéni adatjegyzet-attribútumokat fejleszt, javasoljuk, hogy ne használjon tükröződést az ellenőrzéshez. Ehelyett ajánlott olyan erősen gépelt kódot létrehozni, amely nem támaszkodik a tükröződésre. Ez a megközelítés biztosítja az AOT-buildekkel való zökkenőmentes kompatibilitást.