Note
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de changer d’annuaire.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de changer d’annuaire.
Dans le modèle d’options, différentes méthodes de validation des options sont présentées. Ces méthodes incluent l’utilisation d’attributs d’annotation de données ou l’utilisation d’un validateur personnalisé. Les attributs d’annotation de données sont validés au moment de l’exécution et peuvent entraîner des coûts de performances. Cet article montre comment utiliser le générateur de source de validation des options pour produire du code de validation optimisé au moment de la compilation.
Génération automatique d’implémentation IValidateOptions
L’article sur le modèle d’options montre comment implémenter l’interface IValidateOptions<TOptions> pour valider les options. Le générateur de source de validation des options peut créer automatiquement l’implémentation de l’interface IValidateOptions en tirant parti des attributs d’annotation de données sur la classe options.
Le contenu suivant prend l’exemple d’attributs d’annotation affiché dans le modèle Options et le convertit pour utiliser le générateur de source de validation d’options.
Considérez le fichier appsettings.json suivant :
{
"MyCustomSettingsSection": {
"SiteTitle": "Amazing docs from awesome people!",
"Scale": 10,
"VerbosityLevel": 32
}
}
La classe suivante est liée à la section de configuration "MyCustomSettingsSection" et applique quelques règles 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; }
}
Dans la classe SettingsOptions précédente, la propriété ConfigurationSectionName contient le nom de la section de configuration à lier. Dans ce scénario, l’objet d’options fournit le nom de sa section de configuration. Les attributs d’annotation de données suivants sont utilisés :
- RequiredAttribute: spécifie que la propriété est requise.
- RegularExpressionAttribute: spécifie que la valeur de la propriété doit correspondre au modèle d’expression régulière spécifié.
- RangeAttribute: spécifie que la valeur de la propriété doit se trouver dans une plage spécifiée.
Conseil / Astuce
Outre RequiredAttribute, les propriétés utilisent également le modificateur requis. Cela permet de s’assurer que les consommateurs de l’objet options n’oublient pas de définir la valeur de propriété, bien qu’il ne soit pas lié à la fonctionnalité de génération de source de validation.
Le code suivant illustre comment lier la section de configuration à l’objet options et valider les annotations de données :
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();
Conseil / Astuce
Lorsque la compilation AOT est activée en incluant <PublishAot>true</PublishAot> dans le fichier .csproj, le code peut générer des avertissements tels qu’IL2025 et IL3050. Pour atténuer ces avertissements, il est recommandé d’utiliser le générateur de source de configuration. Pour activer le générateur de source de configuration, ajoutez la propriété <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator> au fichier projet.
En tirant parti de la génération de source au moment de la compilation pour la validation des options, vous pouvez générer du code de validation optimisé pour les performances et éliminer le besoin de réflexion, ce qui entraîne une création d’application compatible AOT plus fluide. Le code suivant montre comment utiliser le générateur de source de validation des options :
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
[OptionsValidator]
public partial class ValidateSettingsOptions : IValidateOptions<SettingsOptions>
{
}
La présence de OptionsValidatorAttribute dans une classe partielle vide indique au générateur de validation des options de créer l'implémentation de l'interface IValidateOptions qui valide SettingsOptions. Le code généré par le générateur de source de validation d’options ressemble à l’exemple suivant :
// <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;
}
}
}
Le code généré est optimisé pour les performances et ne repose pas sur la réflexion. Il est également compatible avec AOT. Le code généré est placé dans un fichier nommé Validators.g.cs.
Remarque
Vous n’avez pas besoin d’effectuer d’autres étapes pour activer le générateur de source de validation des options. Elle est automatiquement activée par défaut lorsque votre projet fait référence à Microsoft.Extensions.Options version 8 ou ultérieure, ou lors de la génération d’une application ASP.NET.
La seule étape à suivre consiste à ajouter les éléments suivants au code de démarrage :
builder.Services
.AddSingleton<IValidateOptions<SettingsOptions>, ValidateSettingsOptions>();
Remarque
L’appel OptionsBuilderDataAnnotationsExtensions.ValidateDataAnnotations<TOptions>(OptionsBuilder<TOptions>) n’est pas nécessaire lors de l’utilisation du générateur de source de validation d’options.
Lorsque l’application tente d’accéder à l’objet options, le code généré pour la validation des options est exécuté pour valider l’objet options. L’extrait de code suivant illustre comment accéder à l’objet options :
var settingsOptions =
app.Services.GetRequiredService<IOptions<SettingsOptions>>().Value;
Attributs d’annotation de données remplacés
Lors de l’examen étroit du code généré, vous remarquerez que les attributs d’annotation de données d’origine, tels que RangeAttribute, initialement appliqués à la propriété SettingsOptions.Scale, ont été remplacés par des attributs personnalisés comme __SourceGen__RangeAttribute. Cette substitution est effectuée parce que RangeAttribute repose sur la réflexion pour la validation. En revanche, __SourceGen__RangeAttribute est un attribut personnalisé optimisé pour les performances et ne dépend pas de la réflexion, ce qui rend le code compatible avec AOT. Le même modèle de remplacement d’attribut sera appliqué sur MaxLengthAttribute, MinLengthAttributeet LengthAttribute en plus de RangeAttribute.
Pour toute personne qui développe des attributs d’annotation de données personnalisés, il est conseillé d’éviter d’utiliser la réflexion pour la validation. Au lieu de cela, il est recommandé d’élaborer du code fortement typé qui ne s’appuie pas sur la réflexion. Cette approche garantit une compatibilité fluide avec les builds AOT.