Options Validation & Named Options
Validate options at startup with DataAnnotations or FluentValidation, and use named options for multiple instances.
Options Validation & Named Options is a free C# Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
Why Validate Options?
Missing or malformed configuration causes runtime errors that are hard to trace. Validating options at startup turns silent config bugs into loud, descriptive exceptions before any request is served.
ValidateOnStart
ValidateOnStart() triggers validation immediately when the app starts, not lazily on first use. This means a misconfigured deployment fails instantly rather than hours later.
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 configDataAnnotations Validation
Use standard System.ComponentModel.DataAnnotations attributes on your options class. They are evaluated automatically by 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();Custom Validation Delegate
The Validate(Func<T, bool>, string) overload adds a lambda for rules that can't be expressed with attributes, like cross-property constraints.
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 for Complex Rules
For complex logic with multiple error messages, implement IValidateOptions<T>. It receives the options instance and returns a result with detailed failure messages.
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>();Named Options — Concept
Named options let you register multiple configurations of the same options type. A common use case: multiple outbound HTTP clients, each with different base URLs and timeouts.
// 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;
}Registering Named Options
Pass a name string as the first argument to Configure<T>. Use IOptionsMonitor<T>.Get(name) to resolve a specific instance.
// 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
}Named Options with Validation
Validate named options individually by calling AddOptions<T>(name) for each named registration.
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 startOptionsBuilder API
OptionsBuilder<T> (returned by AddOptions<T>()) is the fluent API that chains all registration, binding, and validation steps cleanly.
// 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 validationReal-World: Retry Policy Options
A complete retry options setup with cross-field validation and named policies for different services.
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();
}Quick Check
What is the benefit of calling ValidateOnStart() when registering options?
Recap: Options Validation & Named Options
Key takeaways:
ValidateOnStart(): fail at startup instead of at first use- DataAnnotations:
[Required],[Range], etc. +ValidateDataAnnotations() Validate(Func, message): inline lambda for cross-property rulesIValidateOptions<T>: full programmatic validation with multiple errors- Named options:
Configure<T>(name, ...)+IOptionsMonitor<T>.Get(name) OptionsBuilder<T>: fluent API to chain all registration steps
Frequently asked questions
Is the “Options Validation & Named Options” lesson free?
Yes — the full text of “Options Validation & Named Options” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.
What will I learn in “Options Validation & Named Options”?
Validate options at startup with DataAnnotations or FluentValidation, and use named options for multiple instances. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start C# Academy?
No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Options Validation & Named Options” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this C# Academy lesson?
Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Configuration Sources & Providers
- Strongly Typed Options with IOptions
- Options Validation & Named Options
- Secrets Management