اعتبارسنجی تنظیمات در زمان راهاندازی برنامه با IValidateOptions در 11 NET.
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۳/۲۰ ۰۹:۳۰
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
appsettings.json را به کلاسهای سیشارپ (Strongly Typed Classes) نگاشت (Bind) کنیم.null یا 0) را به کلاس مربوطه اختصاص میدهد. این رفتار باعث میشود برنامه بدون مشکل ظاهر شود و خطای اصلی در زمان اجرا (Runtime) و معمولاً در حساسترین لحظات در محیط عملیات (Production) رخ دهد. برای پیادهسازی اصل Fail-Fast (شکست در اولین نقطه ممکن)، داتنت رابط IValidateOptions را ارائه کرده است که در این مقاله به بررسی عمیق و کاربردی آن میپردازیم.public class SmtpOptions
{
public string Host { get; set; } = string.Empty;
public int Port { get; set; }
public string FromAddress { get; set; } = string.Empty;
}Host در فایل appsettings.json تعریف نشده باشد، برنامه بدون هیچ مشکلی راهاندازی میشود. اما به محض اینکه سیستم بخواهد اولین ایمیل را ارسال کند، خطا رخ خواهد داد.[Required] یا [Range] به همراه متد ValidateDataAnnotations() تا حدودی این مشکل را حل میکند، اما در سناریوهای پیچیدهتر زیر کارایی ندارند:UseSsl برابر با true بود، پورت حتماً باید 465 باشد).IValidateOptions برای حل دقیق همین چالشها طراحی شده است.Microsoft.Extensions.Options قرار دارد و ساختار آن بسیار ساده است:public interface IValidateOptions<TOptions> where TOptions : class
{
ValidateOptionsResult Validate(string? name, TOptions options);
}ValidateOnStart()).SmtpOptions به شکل زیر پیادهسازی میشود:using Microsoft.Extensions.Options;
public class SmtpOptionsValidator : IValidateOptions<SmtpOptions>
{
public ValidateOptionsResult Validate(string? name, SmtpOptions options)
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(options.Host))
errors.Add("مشخصه SmtpOptions.Host الزامی است.");
if (options.Port is < 1 or > 65535)
errors.Add($"مقدار پورت باید بین 1 و 65535 باشد. مقدار فعلی: {options.Port}");
if (string.IsNullOrWhiteSpace(options.FromAddress))
errors.Add("مشخصه SmtpOptions.FromAddress الزامی است.");
return errors.Count > 0
? ValidateOptionsResult.Fail(errors)
: ValidateOptionsResult.Success;
}
}Program.cs ثبت کنیم.builder.Services
.Configure<SmtpOptions>(builder.Configuration.GetSection("Smtp"))
.AddSingleton<IValidateOptions<SmtpOptions>, SmtpOptionsValidator>()
.AddOptions<SmtpOptions>()
.ValidateOnStart(); // پرتاب خطا در زمان استارتآپ برنامهنکته کلیدی: فراخوانی متدValidateOnStart()باعث میشود که در صورت وجود هرگونه مغایرت در تنظیمات، خطایOptionsValidationExceptionدقیقا در زمان اجرایapp.Run()پرتاب شود. این کار مانع از بالا آمدن برنامهای با تنظیمات معیوب میگردد.
OptionsBuilder اضافه شده است که فرآیند ثبت را بسیار تمیزتر میکند. این متد به صورت خودکار کلاس اعتبارسنج را از کانتینر DI حل (Resolve) میکند:builder.Services.AddOptions<SmtpOptions>()
.BindConfiguration("Smtp")
.Validate<SmtpOptionsValidator>()
.ValidateOnStart();public class DatabaseOptionsValidator : IValidateOptions<DatabaseOptions>
{
private readonly IHostEnvironment _env;
public DatabaseOptionsValidator(IHostEnvironment env)
{
_env = env;
}
public ValidateOptionsResult Validate(string? name, DatabaseOptions options)
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(options.ConnectionString))
errors.Add("رشته اتصال (ConnectionString) الزامی است.");
// اعمال قوانین سختگیرانهتر در محیط عملیات (Production)
if (!_env.IsDevelopment())
{
if (options.ConnectionString?.Contains("localhost", StringComparison.OrdinalIgnoreCase) == true)
errors.Add("استفاده از localhost در محیط غیر از Development مجاز نیست.");
if (options.MaxPoolSize < 10)
errors.Add("مقدار MaxPoolSize در محیط عملیات باید حداقل 10 باشد.");
}
return errors.Count > 0 ? ValidateOptionsResult.Fail(errors) : ValidateOptionsResult.Success;
}
}public class TlsOptionsValidator : IValidateOptions<TlsOptions>
{
public ValidateOptionsResult Validate(string? name, TlsOptions options)
{
var errors = new List<string>();
if (options.UseTls)
{
if (string.IsNullOrWhiteSpace(options.CertificatePath))
errors.Add("در صورت فعال بودن TLS، مسیر گواهی الزامی است.");
if (options.Port != 443 && options.Port != 8443)
errors.Add("پورتهای مجاز برای TLS تنها 443 یا 8443 هستند.");
}
return errors.Count > 0 ? ValidateOptionsResult.Fail(errors) : ValidateOptionsResult.Success;
}
}name در متد Validate به شما کمک میکند تا اعتبارسنجی را به صورت تفکیکشده اعمال کنید:public class ApiClientOptionsValidator : IValidateOptions<ApiClientOptions>
{
public ValidateOptionsResult Validate(string? name, ApiClientOptions options)
{
var errors = new List<string>();
if (name == "Payments" && options.TimeoutSeconds > 30)
errors.Add($"[{name}] زمان انتظار کلاینت پرداخت نباید بیش از 30 ثانیه باشد.");
return errors.Count > 0 ? ValidateOptionsResult.Fail(errors) : ValidateOptionsResult.Success;
}
}Program.cs کفایت میکند. اما برای پروژههای بزرگ، جداسازی منطق اعتبارسنجی در یک کلاس مجزا به شدت توصیه میشود.| ویژگی | رویکرد درونبرنامهای (Inline Delegate) | رویکرد کلاسمحور (IValidateOptions) |
| میزان تمیزی کد | شلوغ شدن فایل Program.cs | تفکیک وظایف و ساختار منظم |
| تستپذیری | سخت و وابسته به پیکربندی کل وبهاست | تست واحد (Unit Test) بسیار ساده و ایزوله |
| تزریق وابستگی | محدود و دسترسی دشوار به DI | دسترسی کامل و بومی به سازنده کلاس |
| پیچیدگی منطق | مناسب برای بررسیهای تکخطی | مناسب برای منطقهای چندشرطی و پیچیده |
نکته: داتنت به شما اجازه میدهد چندین اعتبارسنج را برای یک کلاس تنظیمات ثبت کنید. فریمورک همه آنها را اجرا کرده و در صورت بروز خطا، مجموعهای از تمام خطاهای رخداده را بازمیگرداند.
IValidateOptions به همراه متد ValidateOnStart() تضمین میکند که اپلیکیشن شما هرگز با یک پیکربندی ناقص یا معیوب راهاندازی نخواهد شد. انتقال زمانِ کشف خطای تنظیمات از «ساعت ۲ بامداد در محیط عملیات» به «ثانیهی اولِ بالا آمدن برنامه در سیستم توسعهدهنده»، دستاورد ارزشمندی است که پایداری نرمافزار شما را به شدت افزایش میدهد. برای اکثر برنامههای سازمانی، سرمایهگذاری روی نوشتن کلاسهای اعتبارسنجی مجزا، هزینههای نگهداری و دیباگ سیستم را در آینده به شکل چشمگیری کاهش خواهد داد.