Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Nel modello di opzioni vengono presentati vari metodi per la convalida delle opzioni. Questi metodi includono l'uso di attributi di annotazione dati o l'uso di un validator personalizzato. Gli attributi di annotazione dei dati vengono convalidati in fase di esecuzione e possono comportare costi di prestazioni. Questo articolo illustra come usare il generatore di origine di convalida delle opzioni per produrre codice di convalida ottimizzato in fase di compilazione.
Generazione automatica dell'implementazione di IValidateOptions
L'articolo relativo al modello di opzioni illustra come implementare l'interfaccia IValidateOptions<TOptions> per la convalida delle opzioni. Il generatore di origine di convalida delle opzioni può creare automaticamente l'implementazione dell'interfaccia IValidateOptions sfruttando gli attributi di annotazione dei dati nella classe options.
Il contenuto seguente accetta l'esempio di attributi di annotazione visualizzato nel modello Options e lo converte in modo da usare il generatore di origine di convalida delle opzioni.
Considerare il file appsettings.json seguente:
{
"MyCustomSettingsSection": {
"SiteTitle": "Amazing docs from awesome people!",
"Scale": 10,
"VerbosityLevel": 32
}
}
La classe seguente viene associata alla sezione di configurazione "MyCustomSettingsSection" e applica un paio di regole 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; }
}
Nella classe SettingsOptions precedente, la proprietà ConfigurationSectionName contiene il nome della sezione di configurazione a cui eseguire l'associazione. In questo scenario, l'oggetto opzioni fornisce il nome della relativa sezione di configurazione. Vengono usati gli attributi di annotazione dati seguenti:
- RequiredAttribute: specifica che la proprietà è obbligatoria.
- RegularExpressionAttribute: specifica che il valore della proprietà deve corrispondere al criterio di espressione regolare specificato.
- RangeAttribute: specifica che il valore della proprietà deve essere compreso in un intervallo specificato.
Suggerimento
Oltre a RequiredAttribute, le proprietà usano anche il modificatore richiesto . Questo aiuta a garantire che gli utenti dell'oggetto delle opzioni non dimentichino di impostare il valore della proprietà, sebbene non sia legato alla funzione di generazione della sorgente di convalida.
Il codice seguente illustra come associare la sezione di configurazione all'oggetto options e convalidare le annotazioni dei dati:
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();
Suggerimento
Quando la compilazione AOT è abilitata includendo <PublishAot>true</PublishAot> nel file con estensione csproj , il codice potrebbe generare avvisi come IL2025 e IL3050. Per attenuare questi avvisi, è consigliabile usare il generatore di codice per la configurazione. Per abilitare il generatore di origine della configurazione, aggiungere la proprietà <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator> al file di progetto.
Sfruttando la generazione di origine in fase di compilazione per la convalida delle opzioni, è possibile generare codice di convalida ottimizzato per le prestazioni ed eliminare la necessità di reflection, con conseguente compilazione di app compatibili con AOT più fluida. Il codice seguente illustra come usare il generatore di origine di convalida delle opzioni:
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
[OptionsValidator]
public partial class ValidateSettingsOptions : IValidateOptions<SettingsOptions>
{
}
La presenza della OptionsValidatorAttribute in una classe parziale vuota istruisce il generatore di convalida delle opzioni dalla sorgente a creare l'implementazione dell'interfaccia IValidateOptions che convalida SettingsOptions. Il codice generato dal generatore di origine di convalida delle opzioni sarà simile all'esempio seguente:
// <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;
}
}
}
Il codice generato è ottimizzato per le prestazioni e non si basa sulla riflessione. È anche compatibile con AOT. Il codice generato viene inserito in un file denominato Validators.g.cs.
Annotazioni
Non è necessario eseguire altri passaggi per abilitare il generatore di origine di convalida delle opzioni. Viene abilitato automaticamente per impostazione predefinita quando il progetto fa riferimento a Microsoft.Extensions.Options versione 8 o successiva o quando si compila un'applicazione ASP.NET.
L'unico passaggio da eseguire consiste nell'aggiungere quanto segue al codice di avvio:
builder.Services
.AddSingleton<IValidateOptions<SettingsOptions>, ValidateSettingsOptions>();
Annotazioni
La chiamata OptionsBuilderDataAnnotationsExtensions.ValidateDataAnnotations<TOptions>(OptionsBuilder<TOptions>) non è necessaria quando si usa il generatore di origine di convalida delle opzioni.
Quando l'applicazione tenta di accedere all'oggetto opzioni, il codice generato per la convalida delle opzioni viene eseguito per convalidare l'oggetto opzioni. Il frammento di codice seguente illustra come accedere all'oggetto options:
var settingsOptions =
app.Services.GetRequiredService<IOptions<SettingsOptions>>().Value;
Attributi di annotazione dati sostituiti
Al termine dell'esame del codice generato, si noterà che gli attributi di annotazione dei dati originali, ad esempio RangeAttribute, che sono stati inizialmente applicati alla proprietà SettingsOptions.Scale, sono stati sostituiti con attributi __SourceGen__RangeAttributepersonalizzati come . Questa sostituzione viene eseguita perché RangeAttribute si basa sulla riflessione per la convalida. Al contrario, __SourceGen__RangeAttribute è un attributo personalizzato ottimizzato per le prestazioni e non dipende dalla reflection, rendendo il codice compatibile con AOT. Lo stesso modello di sostituzione degli attributi verrà applicato a MaxLengthAttribute, MinLengthAttributee LengthAttribute oltre a RangeAttribute.
Per chiunque sviluppi attributi di annotazione dati personalizzati, è consigliabile evitare di usare la reflection per la convalida. È invece consigliabile creare codice fortemente tipizzato che non si basa sulla riflessione. Questo approccio garantisce una compatibilità uniforme con le compilazioni AOT.