0Pricing
C# Academy · 课时

选项验证与命名选项

使用 DataAnnotations 或 FluentValidation 在启动时验证选项,并使用命名选项支持多个实例。

选项验证与命名选项 是 CoddyKit 上的免费 C# Academy 课时。 这是第 3 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 C# Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 C# Academy 课程共包含 4 节课。

为什么要验证选项?

缺失或格式错误的配置会导致难以追踪的运行时错误。在启动时验证选项,可以在处理任何请求之前,将隐蔽的配置错误转变为明确且详细的异常。

ValidateOnStart

ValidateOnStart() 会在应用启动时立即触发验证,而不是等到第一次使用时才延迟验证。这意味着配置错误的部署会立即失败,而不是运行数小时后才失败。

builder.Services
    .AddOptions<DatabaseOptions>()
    .BindConfiguration("Database")
    .ValidateDataAnnotations()
    .ValidateOnStart(); // throw OptionsValidationException on startup

// Without ValidateOnStart:
// - Validation only runs on first IOptions<T>.Value access
// - A rarely-used service could run for hours before failing

// With ValidateOnStart:
// - App fails at Host.Run() if config is invalid
// - Health checks and probes never report healthy for bad config

DataAnnotations 验证

在选项类上使用标准的 System.ComponentModel.DataAnnotations 特性。ValidateDataAnnotations() 会自动对这些特性进行评估。

using System.ComponentModel.DataAnnotations;

public class EmailOptions
{
    [Required(ErrorMessage = "SMTP host is required")]
    [MinLength(3)]
    public string SmtpHost { get; set; } = string.Empty;

    [Range(1, 65535, ErrorMessage = "Port must be 1-65535")]
    public int SmtpPort { get; set; } = 587;

    [Required]
    [EmailAddress(ErrorMessage = "Invalid sender address")]
    public string SenderAddress { get; set; } = string.Empty;

    [Range(1, 30)]
    public int TimeoutSeconds { get; set; } = 10;
}

builder.Services
    .AddOptions<EmailOptions>()
    .BindConfiguration("Email")
    .ValidateDataAnnotations()
    .ValidateOnStart();

自定义验证委托

Validate(Func<T, bool>, string) 重载会添加一个 Lambda,用于处理无法通过特性表达的规则,例如跨属性约束。

builder.Services
    .AddOptions<ConnectionPoolOptions>()
    .BindConfiguration("ConnectionPool")
    .ValidateDataAnnotations()
    .Validate(
        opts => opts.MaxSize >= opts.MinSize,
        "MaxSize must be greater than or equal to MinSize")
    .Validate(
        opts => opts.ConnectionTimeoutMs > 0,
        "ConnectionTimeoutMs must be positive")
    .ValidateOnStart();

public class ConnectionPoolOptions
{
    [Range(1, 100)] public int MinSize { get; set; } = 2;
    [Range(1, 500)] public int MaxSize { get; set; } = 20;
    public int ConnectionTimeoutMs { get; set; } = 5000;
}

使用 IValidateOptions 处理复杂规则

对于包含多个错误消息的复杂逻辑,请实现 IValidateOptions<T>。它接收选项实例,并返回包含详细失败消息的结果。

public class PaymentOptionsValidator : IValidateOptions<PaymentOptions>
{
    public ValidateOptionsResult Validate(string? name, PaymentOptions opts)
    {
        var failures = new List<string>();

        if (opts.Provider == "Stripe" && string.IsNullOrWhiteSpace(opts.StripeSecretKey))
            failures.Add("StripeSecretKey is required when Provider is Stripe");

        if (opts.Provider == "PayPal" && string.IsNullOrWhiteSpace(opts.PayPalClientId))
            failures.Add("PayPalClientId is required when Provider is PayPal");

        if (opts.RetryCount < 0 || opts.RetryCount > 5)
            failures.Add("RetryCount must be between 0 and 5");

        return failures.Count == 0
            ? ValidateOptionsResult.Success
            : ValidateOptionsResult.Fail(failures);
    }
}

builder.Services.AddSingleton<IValidateOptions<PaymentOptions>, PaymentOptionsValidator>();

命名选项——概念

命名选项允许您注册同一选项类型的多个配置。一个常见场景是多个出站 HTTP 客户端,每个客户端具有不同的基础 URL 和超时设置。

// appsettings.json:
{
  "HttpClients": {
    "Orders": {
      "BaseUrl": "https://orders-service",
      "TimeoutSeconds": 30
    },
    "Inventory": {
      "BaseUrl": "https://inventory-service",
      "TimeoutSeconds": 10
    }
  }
}

public class HttpClientOptions
{
    public string BaseUrl { get; set; } = string.Empty;
    public int TimeoutSeconds { get; set; } = 30;
}

注册命名选项

将名称字符串作为 Configure<T> 的第一个参数传入。使用 IOptionsMonitor<T>.Get(name) 解析特定实例。

// Register:
builder.Services.Configure<HttpClientOptions>("Orders",
    builder.Configuration.GetSection("HttpClients:Orders"));
builder.Services.Configure<HttpClientOptions>("Inventory",
    builder.Configuration.GetSection("HttpClients:Inventory"));

// Consume:
public class ApiGateway
{
    private readonly HttpClientOptions _orders;
    private readonly HttpClientOptions _inventory;

    public ApiGateway(IOptionsMonitor<HttpClientOptions> monitor)
    {
        _orders    = monitor.Get("Orders");
        _inventory = monitor.Get("Inventory");
    }

    // IOptions<T>.Value always returns the unnamed (default) instance
    // IOptionsMonitor<T>.Get(name) returns the named instance
}

验证命名选项

对每个命名注册调用 AddOptions<T>(name),即可分别验证命名选项。

foreach (var clientName in new[] { "Orders", "Inventory", "Auth" })
{
    builder.Services
        .AddOptions<HttpClientOptions>(clientName)
        .BindConfiguration($"HttpClients:{clientName}")
        .ValidateDataAnnotations()
        .Validate(
            opts => Uri.IsWellFormedUriString(opts.BaseUrl, UriKind.Absolute),
            $"HttpClients:{clientName}:BaseUrl must be a valid absolute URI")
        .ValidateOnStart();
}

// If any named instance fails, the app refuses to start

OptionsBuilder API

OptionsBuilder<T>(由 AddOptions<T>() 返回)是流式 API,可整洁地串联所有注册、绑定和验证步骤。

// Full OptionsBuilder chain:
builder.Services
    .AddOptions<DatabaseOptions>()        // create builder
    .BindConfiguration("Database")         // bind JSON section
    .Configure(opts =>                     // manual override
    {
        if (builder.Environment.IsDevelopment())
            opts.EnableDetailedErrors = true;
    })
    .PostConfigure(opts =>                 // runs after all Configure
    {
        opts.ConnectionString ??= "default-fallback";
    })
    .ValidateDataAnnotations()             // attribute rules
    .Validate(o => o.MaxPoolSize > 0,
              "MaxPoolSize must be positive")
    .ValidateOnStart();                    // eager validation

实战:重试策略选项

一个完整的重试选项配置方案,包含跨字段验证,以及针对不同服务的命名策略。

public class RetryOptions
{
    [Range(0, 10)] public int MaxAttempts { get; set; } = 3;
    [Range(100, 60000)] public int BaseDelayMs { get; set; } = 500;
    public bool UseExponentialBackoff { get; set; } = true;
    [Range(1, 120000)] public int MaxDelayMs { get; set; } = 30000;
}

foreach (var policy in new[] { "Database", "HttpClient", "MessageBus" })
{
    builder.Services
        .AddOptions<RetryOptions>(policy)
        .BindConfiguration($"RetryPolicies:{policy}")
        .Validate(o => !o.UseExponentialBackoff || o.MaxDelayMs > o.BaseDelayMs,
                  "MaxDelayMs must exceed BaseDelayMs when using exponential backoff")
        .ValidateOnStart();
}

快速检查

注册选项时调用 ValidateOnStart() 有什么好处?

回顾:选项验证与命名选项

要点:

  • ValidateOnStart():在启动时失败,而不是等到第一次使用时才失败
  • DataAnnotations:[Required]、[Range] 等 + ValidateDataAnnotations()
  • Validate(Func, message):用于跨属性规则的内联 Lambda
  • IValidateOptions<T>:支持多个错误的完整程序化验证
  • 命名选项:Configure<T>(name, ...) + IOptionsMonitor<T>.Get(name)
  • OptionsBuilder<T>:用于串联所有注册步骤的流式 API

常见问题解答

「选项验证与命名选项」课时是免费的吗?

是的 — 「选项验证与命名选项」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 C# Academy 课程的其余内容,请升级到 CoddyKit PRO。 C# Academy 课程共包含 4 节课。

「选项验证与命名选项」这节课中我会学到什么?

使用 DataAnnotations 或 FluentValidation 在启动时验证选项,并使用命名选项支持多个实例。 你通过在浏览器中直接运行的动手代码来练习 C# Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 C# Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 C# Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 3 节课,共 4 节。

「选项验证与命名选项」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 C# Academy 课中编写并运行代码吗?

能。每节 C# Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 配置源与提供程序
  2. 使用 IOptions 的强类型选项
  3. 选项验证与命名选项
  4. 机密管理
← 返回 C# Academy