عنوان:

‫اعتبارسنجی تنظیمات در زمان راه‌اندازی برنامه با IValidateOptions در 11 NET.


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۳/۲۰ ۰۹:۳۰
آدرس: www.dntips.ir
در توسعه برنامه‌های مدرن با فریم‌ورک .NET، مدیریت تنظیمات (Configuration Management) با استفاده از الگوی تزریق گزینه‌ها (Options Pattern) یکی از اصول کلیدی است. این الگو به ما اجازه می‌دهد تا بخش‌های مختلف فایل‌های تنظیمات مانند appsettings.json را به کلاس‌های سی‌شارپ (Strongly Typed Classes) نگاشت (Bind) کنیم.
با این حال، «نگاشت تنظیمات» لزوماً به معنای «صحت» آن‌ها نیست. اگر یک کلید حیاتی در فایل تنظیمات فراموش شود یا مقدار آن خارج از محدوده مجاز باشد، مکانیزم پیش‌فرض دات‌نت بدون هیچ خطایی، مقادیر پیش‌فرض (مانند null یا 0) را به کلاس مربوطه اختصاص می‌دهد. این رفتار باعث می‌شود برنامه بدون مشکل ظاهر شود و خطای اصلی در زمان اجرا (Runtime) و معمولاً در حساس‌ترین لحظات در محیط عملیات (Production) رخ دهد. برای پیاده‌سازی اصل Fail-Fast (شکست در اولین نقطه ممکن)، دات‌نت رابط IValidateOptions را ارائه‌ کرده است که در این مقاله به بررسی عمیق و کاربردی آن می‌پردازیم.

صورت مسئله: تنظیمات اشتباه و خطاهای پنهان
فرض کنید یک کلاس تنظیمات برای ارسال ایمیل (SMTP) به شکل زیر تعریف کرده‌ایم:
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 تعریف نشده باشد، برنامه بدون هیچ مشکلی راه‌اندازی می‌شود. اما به محض اینکه سیستم بخواهد اولین ایمیل را ارسال کند، خطا رخ خواهد داد.
استفاده از مشخصه‌های اعتبار‌سنجی (Data Annotations) مانند [Required] یا [Range] به همراه متد ValidateDataAnnotations() تا حدودی این مشکل را حل می‌کند، اما در سناریوهای پیچیده‌تر زیر کارایی ندارند:
  • اعتبارسنجی متقاطع (Cross-Property Validation): زمانی که صحت یک ویژگی به ویژگی دیگری وابسته است (مثلاً اگر UseSsl برابر با true بود، پورت حتماً باید 465 باشد).
  • بررسی‌های ناهمگام (Async) یا وابسته به دیتابیس.
  • اعتبارسنجی‌های مشروط بر اساس محیط برنامه (Environment-specific logic).
  • استفاده مجدد از منطق اعتبارسنجی در چند کلاس مختلف.
رابط IValidateOptions برای حل دقیق همین چالش‌ها طراحی شده است.

رابط IValidateOptions چیست؟
این اینترفیس در فضای نام Microsoft.Extensions.Options قرار دارد و ساختار آن بسیار ساده است:
public interface IValidateOptions<TOptions> where TOptions : class
{
    ValidateOptionsResult Validate(string? name, TOptions options);
}
شما این رابط را در یک کلاس مجزا پیاده‌سازی کرده و آن را در کانتینر تزریق وابستگی (DI Container) ثبت می‌کنید. سیستم مدیریت تنظیمات دات‌نت، این اعتبارسنجی را به دو صورت انجام می‌دهد: به صورت تنبل (Lazy - در اولین فراخوانی) یا به صورت فوری در زمان بالا آمدن برنامه (با استفاده از متد 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 ثبت کنیم.

روش اول: ثبت به عنوان یک سرویس مجزا
در نسخه‌های کلاسیک، کلاس اعتبارسنج به عنوان یک Singleton در بخش تزریق وابستگی‌ها ثبت می‌شد:
builder.Services
    .Configure<SmtpOptions>(builder.Configuration.GetSection("Smtp"))
    .AddSingleton<IValidateOptions<SmtpOptions>, SmtpOptionsValidator>()
    .AddOptions<SmtpOptions>()
    .ValidateOnStart(); // پرتاب خطا در زمان استارت‌آپ برنامه
نکته کلیدی: فراخوانی متد ValidateOnStart() باعث می‌شود که در صورت وجود هرگونه مغایرت در تنظیمات، خطای OptionsValidationException دقیقا در زمان اجرای app.Run() پرتاب شود. این کار مانع از بالا آمدن برنامه‌ای با تنظیمات معیوب می‌گردد.

روش جدید در دات‌نت (Fluent API):
در نسخه‌های اخیر دات‌نت 11، متد زنجیره‌ایِ جدیدی تحت عنوان به ساختار OptionsBuilder اضافه شده است که فرآیند ثبت را بسیار تمیزتر می‌کند. این متد به صورت خودکار کلاس اعتبارسنج را از کانتینر DI حل (Resolve) می‌کند:
builder.Services.AddOptions<SmtpOptions>()
    .BindConfiguration("Smtp")
    .Validate<SmtpOptionsValidator>()
    .ValidateOnStart();

قابلیت‌های پیشرفته IValidateOptions
۱. دسترسی به سایر سرویس‌ها (تزریق وابستگی)
از آنجا که کلاس اعتبارسنج یک شهروند درجه‌یک در سیستم DI دات‌نت است، شما می‌توانید هر سرویس دیگری (مانند اطلاعات محیط برنامه یا لاگرها) را از طریق سازنده (Constructor) به آن تزریق کنید. این مزیتی است که به هیچ وجه در Data Annotations وجود ندارد.
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;
    }
}

۲. اعتبارسنجی متقاطع (Cross-Property)
همان‌طور که پیش‌تر اشاره شد، بررسی روابط منطقی بین ویژگی‌های مختلف یک کلاس تنظیمات، با این ابزار به سادگی امکان‌پذیر است:
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;
    }
}

۳. پشتیبانی از تنظیمات نام‌گذاری شده (Named Options)
اگر در برنامه خود چندین نمونه از یک کلاس تنظیمات دارید (مثلاً دو کلاینت مختلف برای دو API مجزا)، ورودی 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;
    }
}

بررسی رویکرد سنتی (Inline Delegate) در مقابل رویکرد کلاس‌محور
برای بررسی‌های بسیار ساده، استفاده از یک عبارت لامبدا (Lambda) در همان فایل Program.cs کفایت می‌کند. اما برای پروژه‌های بزرگ، جداسازی منطق اعتبارسنجی در یک کلاس مجزا به شدت توصیه می‌شود.
ویژگیرویکرد درون‌برنامه‌ای (Inline Delegate)رویکرد کلاس‌محور (IValidateOptions)
میزان تمیزی کدشلوغ شدن فایل Program.csتفکیک وظایف و ساختار منظم
تست‌پذیریسخت و وابسته به پیکربندی کل وب‌هاستتست واحد (Unit Test) بسیار ساده و ایزوله
تزریق وابستگیمحدود و دسترسی دشوار به DIدسترسی کامل و بومی به سازنده کلاس
پیچیدگی منطقمناسب برای بررسی‌های تک‌خطیمناسب برای منطق‌های چندشرطی و پیچیده
نکته: دات‌نت به شما اجازه می‌دهد چندین اعتبارسنج را برای یک کلاس تنظیمات ثبت کنید. فریم‌ورک همه آن‌ها را اجرا کرده و در صورت بروز خطا، مجموعه‌ای از تمام خطاهای رخ‌داده را بازمی‌گرداند.

نتیجه‌گیری
استفاده از IValidateOptions به همراه متد ValidateOnStart() تضمین می‌کند که اپلیکیشن شما هرگز با یک پیکربندی ناقص یا معیوب راه‌اندازی نخواهد شد. انتقال زمانِ کشف خطای تنظیمات از «ساعت ۲ بامداد در محیط عملیات» به «ثانیه‌ی اولِ بالا آمدن برنامه در سیستم توسعه‌دهنده»، دستاورد ارزشمندی است که پایداری نرم‌افزار شما را به شدت افزایش می‌دهد. برای اکثر برنامه‌های سازمانی، سرمایه‌گذاری روی نوشتن کلاس‌های اعتبارسنجی مجزا، هزینه‌های نگهداری و دیباگ سیستم را در آینده به شکل چشمگیری کاهش خواهد داد.