Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Dalam pola opsi, berbagai metode untuk memvalidasi opsi disajikan. Metode ini termasuk menggunakan atribut anotasi data atau menggunakan validator kustom. Atribut anotasi data divalidasi pada waktu eksekusi dan dapat menimbulkan beban performa. Artikel ini menunjukkan cara menggunakan generator sumber validasi opsi untuk menghasilkan kode validasi yang dioptimalkan pada waktu kompilasi.
Pembuatan implementasi IValidateOptions otomatis
Artikel pola opsi menggambarkan IValidateOptions<TOptions> cara mengimplementasikan antarmuka untuk memvalidasi opsi. Generator sumber validasi opsi dapat secara otomatis membuat IValidateOptions implementasi antarmuka dengan memanfaatkan atribut anotasi data pada kelas opsi.
Konten berikut mengambil contoh atribut anotasi yang ditampilkan dalam pola Opsi dan mengonversinya untuk menggunakan generator sumber validasi opsi.
Pertimbangkan file appsettings.json berikut:
{
"MyCustomSettingsSection": {
"SiteTitle": "Amazing docs from awesome people!",
"Scale": 10,
"VerbosityLevel": 32
}
}
Kelas berikut mengikat ke bagian "MyCustomSettingsSection" konfigurasi dan menerapkan beberapa DataAnnotations aturan:
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; }
}
Di kelas sebelumnya SettingsOptions , ConfigurationSectionName properti berisi nama bagian konfigurasi yang akan diikat. Dalam skenario ini, objek opsi menyediakan nama bagian konfigurasinya. Atribut anotasi data berikut digunakan:
- RequiredAttribute: Menentukan bahwa properti diperlukan.
- RegularExpressionAttribute: Menentukan bahwa nilai properti harus cocok dengan pola ekspresi reguler yang ditentukan.
- RangeAttribute: Menentukan bahwa nilai properti harus berada dalam rentang tertentu.
Petunjuk / Saran
Selain RequiredAttribute, properti juga menggunakan pengubah yang diperlukan . Ini membantu memastikan bahwa pengguna objek opsi tidak lupa untuk mengatur nilai properti, meskipun hal ini tidak berkaitan dengan fitur penghasilan sumber validasi.
Kode berikut mencontohkan cara mengikat bagian konfigurasi ke objek opsi dan memvalidasi anotasi data:
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();
Petunjuk / Saran
Ketika kompilasi AOT diaktifkan dengan menyertakan <PublishAot>true</PublishAot> dalam file .csproj , kode mungkin menghasilkan peringatan seperti IL2025 dan IL3050. Untuk mengurangi peringatan ini, disarankan untuk menggunakan generator sumber konfigurasi. Untuk mengaktifkan generator sumber konfigurasi, tambahkan properti <EnableConfigurationBindingGenerator>true</EnableConfigurationBindingGenerator> ke file proyek.
Dengan memanfaatkan generasi sumber saat waktu kompilasi untuk validasi opsi, Anda dapat menghasilkan kode validasi yang dioptimalkan untuk kinerja dan menghilangkan kebutuhan akan refleksi, yang menghasilkan pembuatan aplikasi yang lebih kompatibel dengan AOT dan lebih lancar. Kode berikut menunjukkan cara menggunakan generator sumber validasi opsi:
using Microsoft.Extensions.Options;
namespace ConsoleJson.Example;
[OptionsValidator]
public partial class ValidateSettingsOptions : IValidateOptions<SettingsOptions>
{
}
Kehadiran OptionsValidatorAttribute pada kelas parsial kosong menginstruksikan generator sumber validasi opsi untuk membuat IValidateOptions implementasi antarmuka yang memvalidasi SettingsOptions. Kode yang dihasilkan oleh generator sumber validasi opsi akan menyerupai contoh berikut:
// <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;
}
}
}
Kode yang dihasilkan dioptimalkan untuk performa dan tidak bergantung pada pantulan. Ini juga kompatibel dengan AOT. Kode yang dihasilkan ditempatkan dalam file bernama Validators.g.cs.
Nota
Anda tidak perlu mengambil langkah tambahan untuk mengaktifkan generator sumber validasi opsi. Ini secara otomatis diaktifkan secara default saat proyek Anda mereferensikan Microsoft.Extensions.Options versi 8 atau yang lebih baru, atau saat membangun aplikasi ASP.NET.
Satu-satunya langkah yang perlu Anda ambil adalah menambahkan yang berikut ke kode startup:
builder.Services
.AddSingleton<IValidateOptions<SettingsOptions>, ValidateSettingsOptions>();
Nota
Tidak perlu memanggil OptionsBuilderDataAnnotationsExtensions.ValidateDataAnnotations<TOptions>(OptionsBuilder<TOptions>) saat menggunakan generator sumber validasi opsi.
Ketika aplikasi mencoba mengakses objek opsi, kode yang dihasilkan untuk validasi opsi dijalankan untuk memvalidasi objek opsi. Cuplikan kode berikut menggambarkan cara mengakses objek opsi:
var settingsOptions =
app.Services.GetRequiredService<IOptions<SettingsOptions>>().Value;
Atribut anotasi data yang diganti
Setelah pemeriksaan dekat kode yang dihasilkan, Anda akan mengamati bahwa atribut anotasi data asli, seperti RangeAttribute, yang awalnya diterapkan ke properti SettingsOptions.Scale, telah diganti dengan atribut kustom seperti __SourceGen__RangeAttribute. Penggantian ini dibuat karena RangeAttribute bergantung pada refleksi untuk validasi. Sebaliknya, __SourceGen__RangeAttribute adalah atribut kustom yang dioptimalkan untuk performa dan tidak bergantung pada pantulan, membuat kode kompatibel dengan AOT. Pola penggantian atribut yang sama akan diterapkan pada MaxLengthAttribute, , MinLengthAttributedan LengthAttribute selain RangeAttribute.
Bagi siapa pun yang mengembangkan atribut anotasi data kustom, disarankan untuk menahan diri untuk tidak menggunakan refleksi untuk validasi. Sebaliknya, disarankan untuk membuat kode yang bertipe kuat yang tidak bergantung pada refleksi. Pendekatan ini memastikan kompatibilitas yang lancar dengan build AOT.