Hinweis
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, sich anzumelden oder das Verzeichnis zu wechseln.
Für den Zugriff auf diese Seite ist eine Autorisierung erforderlich. Sie können versuchen, das Verzeichnis zu wechseln.
Im Optionsmuster werden verschiedene Methoden zum Überprüfen von Optionen dargestellt. Diese Methoden umfassen die Verwendung von Datenanmerkungsattributen oder das Verwenden eines benutzerdefinierten Validators. Datenanmerkungsattribute werden zur Laufzeit überprüft und können Leistungskosten verursachen. In diesem Artikel wird veranschaulicht, wie Sie den Optionsüberprüfungsquellgenerator verwenden, um zur Kompilierungszeit einen optimierten Validierungscode zu erstellen.
Automatische IValidateOptions-Implementierungsgenerierung
Der Optionsmusterartikel veranschaulicht, wie die Schnittstelle für die IValidateOptions<TOptions> Überprüfung von Optionen implementiert wird. Der Quellengenerator für die Optionsvalidierung kann die IValidateOptions
-Schnittstellenimplementierung automatisch erstellen, indem Datenanmerkungsattribute für die Optionsklasse verwendet werden.
Der folgende Inhalt verwendet das Beispiel für Anmerkungsattribute, das im Optionsmuster angezeigt wird, und konvertiert ihn für die Verwendung des Quellengenerator für die Optionsvalidierung.
Sehen Sie sich die nachfolgende Datei appsettings.json an:
{
"MyCustomSettingsSection": {
"SiteTitle": "Amazing docs from awesome people!",
"Scale": 10,
"VerbosityLevel": 32
}
}
Die folgende Klasse erstellt eine Bindung zum "MyCustomSettingsSection"
-Konfigurationsabschnitt und wendet einige DataAnnotations
-Regeln an:
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; }
}
In der vorangehenden SettingsOptions
-Klasse enthält die Eigenschaft ConfigurationSectionName
den Namen des Konfigurationsabschnitts, an die er gebunden werden soll. In diesem Szenario stellt das Optionsobjekt den Namen seines Konfigurationsabschnitts zur Verfügung. Die folgenden Datenanmerkungsattribute werden verwendet:
- RequiredAttribute: Gibt an, dass die Eigenschaft erforderlich ist.
- RegularExpressionAttribute: Gibt an, dass der Eigenschaftswert mit dem angegebenen Muster für reguläre Ausdrücke übereinstimmen muss.
- RangeAttribute: Gibt an, dass der Eigenschaftswert innerhalb eines angegebenen Bereichs liegen muss.
Tipp
Zusätzlich zu RequiredAttribute
verwenden die Eigenschaften auch den erforderlichen Modifizierer. Dadurch wird sichergestellt, dass Verbraucher des Optionsobjekts nicht vergessen, den Eigenschaftswert festzulegen, obwohl es nicht mit dem Feature zur Überprüfungsquellengenerierung in Beziehung steht.
Der folgende Code veranschaulicht, wie der Konfigurationsabschnitt an das Optionsobjekt gebunden und die Datenanmerkungen überprüft werden:
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();
Tipp
Wenn die AOT-Kompilierung aktiviert ist, indem sie in die <PublishAot>true</PublishAot>
eingeschlossen wird, generiert der Code möglicherweise Warnungen wie IL2025 und IL3050. Um diese Warnungen zu vermeiden, empfiehlt es sich, den Konfigurationsquellengenerator zu verwenden. Um den Konfigurationsquellgenerator zu aktivieren, fügen Sie der Projektdatei die Eigenschaft <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator>
hinzu.
Durch die Nutzung der Erstellung von Kompilierungszeitquellen für die Überprüfung von Optionen können Sie leistungsoptimierten Validierungscode generieren und die Notwendigkeit der Reflexion vermeiden, was zu einer reibungsloseren AOT-kompatiblen App-Erstellung führt. Der folgende Code veranschaulicht die Verwendung des Quellengenerators für die Optionsüberprüfung:
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
[OptionsValidator]
public partial class ValidateSettingsOptions : IValidateOptions<SettingsOptions>
{
}
Das Vorhandensein der OptionsValidatorAttribute in einer leeren Teilklasse weist den Optionsvalidierungsquellengenerator an, die Schnittstellenimplementierung zu erstellen, die IValidateOptions
überprüft und SettingsOptions
validiert. Der code, der vom Optionsüberprüfungsquellgenerator generiert wird, ähnelt dem folgenden Beispiel:
// <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;
}
}
}
Der generierte Code ist für die Leistung optimiert und basiert nicht auf Spiegelung. Es ist auch AOT-kompatibel. Der generierte Code wird in einer Datei mit dem Namen Validators.g.cs platziert.
Hinweis
Sie müssen keine zusätzlichen Schritte ausführen, um den Optionsüberprüfungsquellengenerator zu aktivieren. Sie ist standardmäßig aktiviert, wenn Ihr Projekt auf Microsoft.Extensions.Options Version 8 oder höher verweist oder wenn Sie eine ASP.NET Anwendung erstellen.
Der einzige Schritt, den Sie ausführen müssen, besteht darin, dem Startcode Folgendes hinzuzufügen:
builder.Services
.AddSingleton<IValidateOptions<SettingsOptions>, ValidateSettingsOptions>();
Hinweis
Der Aufruf OptionsBuilderDataAnnotationsExtensions.ValidateDataAnnotations<TOptions>(OptionsBuilder<TOptions>) ist nicht erforderlich, wenn der Optionsüberprüfungsquellengenerator verwendet wird.
Wenn die Anwendung versucht, auf das Optionsobjekt zuzugreifen, wird der generierte Code für die Optionsüberprüfung ausgeführt, um das Optionsobjekt zu überprüfen. Der folgende Codeausschnitt veranschaulicht, wie auf das Optionsobjekt zugegriffen wird:
var settingsOptions =
app.Services.GetRequiredService<IOptions<SettingsOptions>>().Value;
Ersetzte Datenanmerkungsattribute
Bei der genauen Untersuchung des generierten Codes werden Sie feststellen, dass die ursprünglichen Datenanmerkungsattribute, wie z. B. RangeAttribute, die ursprünglich auf die Eigenschaft SettingsOptions.Scale
angewendet wurden, durch benutzerdefinierte Attribute, wie z. B. __SourceGen__RangeAttribute
, ersetzt wurden. Diese Ersetzung erfolgt, da RangeAttribute
für die Validierung auf Reflexion beruht. Im Gegensatz dazu ist __SourceGen__RangeAttribute
ein benutzerdefiniertes Attribut, das für die Leistung optimiert ist und nicht von der Reflexion abhängt, wodurch der Code AOT-kompatibel wird. Das gleiche Muster der Attributersetzung wird auf MaxLengthAttribute, MinLengthAttribute, und LengthAttribute, zusätzlich zu RangeAttribute, angewendet.
Für alle Entwickler, die benutzerdefinierte Datenanmerkungsattribute entwickeln, empfiehlt es sich, die Reflexion nicht zur Überprüfung zu verwenden. Stattdessen wird empfohlen, stark typierten Code zu erstellen, der nicht auf Reflexion basiert. Dieser Ansatz sorgt für eine reibungslose Kompatibilität mit AOT-Builds.