ASP.NET Core Web APIs · Configuration, Security, Data and Operations
Validated Options and Fail-Fast Configuration
Bind configuration to typed options, validate it with data annotations and ValidateOnStart, and choose between IOptions, IOptionsSnapshot and IOptionsMonitor.
What you'll build
A PaymentsOptions class bound from the Payments configuration section, validated with data annotations and a custom rule, and checked at startup with ValidateOnStart(), so a missing API URL stops the app before it takes traffic instead of failing on the first payment.
The idea
- Configuration comes from appsettings files, environment variables (
Payments__BaseUrl), secrets stores and the command line, merged in order. AddOptions<T>().Bind(section)maps a section to a class;ValidateDataAnnotations()andValidate(...)add rules.ValidateOnStart()runs validation when the host starts; without it, invalid options throw only when first used.- Inject
IOptions<T>(read once),IOptionsSnapshot<T>(per request, scoped) orIOptionsMonitor<T>(current value plus change notifications, usable from singletons).
The code
using System.ComponentModel.DataAnnotations;
using Microsoft.Extensions.Options;
foreach (var (label, settings) in new[]
{
("valid config", new Dictionary<string, string?> { ["Payments:BaseUrl"] = "https://api.payments.example", ["Payments:TimeoutSeconds"] = "10", ["Payments:Retries"] = "3" }),
("missing url", new Dictionary<string, string?> { ["Payments:TimeoutSeconds"] = "10" }),
("retries vs timeout", new Dictionary<string, string?> { ["Payments:BaseUrl"] = "https://api.payments.example", ["Payments:TimeoutSeconds"] = "2", ["Payments:Retries"] = "5" }),
})
{
var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders(); // the example prints the validation failures itself
builder.Configuration.AddInMemoryCollection(settings);
builder.Services.AddOptions<PaymentsOptions>()
.Bind(builder.Configuration.GetSection("Payments"))
.ValidateDataAnnotations()
.Validate(o => o.Retries * 2 <= o.TimeoutSeconds * 3, "Retries must fit in the timeout budget")
.ValidateOnStart();
var app = builder.Build();
app.MapGet("/config", (IOptions<PaymentsOptions> options) => options.Value);
app.Urls.Add("http://127.0.0.1:0");
try
{
await app.StartAsync();
using var http = new HttpClient { BaseAddress = new Uri(app.Urls.First()) };
Console.WriteLine($"{label}: started, /config = {await http.GetStringAsync("/config")}");
await app.StopAsync();
}
catch (OptionsValidationException e)
{
Console.WriteLine($"{label}: refused to start -> {string.Join("; ", e.Failures)}");
}
}
public class PaymentsOptions
{
[Required, Url] public string BaseUrl { get; set; } = "";
[Range(1, 60)] public int TimeoutSeconds { get; set; } = 10;
[Range(0, 5)] public int Retries { get; set; }
}
Output
valid config: started, /config = {"baseUrl":"https://api.payments.example","timeoutSeconds":10,"retries":3}
missing url: refused to start -> DataAnnotation validation failed for 'PaymentsOptions' members: 'BaseUrl' with the error: 'The BaseUrl field is required.'.
retries vs timeout: refused to start -> Retries must fit in the timeout budgetRun this on your computer — see “Get Started” for setup.
Common mistakes
- Reading
builder.Configuration["Payments:BaseUrl"]with string keys all over the code instead of typed options. - Validating options but forgetting
ValidateOnStart(), so a broken deployment passes health checks and fails later. - Injecting
IOptionsSnapshot<T>into a singleton (it's scoped); useIOptionsMonitor<T>there. - Committing secrets to appsettings.json; use environment variables or a secrets store.
Key points
- Bind configuration sections to typed options classes.
- Validate with data annotations and custom rules, and fail at startup with ValidateOnStart.
- Pick IOptions, IOptionsSnapshot or IOptionsMonitor by lifetime and reload needs.
Further reading: Options pattern in ASP.NET Core · Configuration in ASP.NET Core
Practice questions
What does ValidateOnStart do?
Answer
It validates the options when the host starts, so invalid configuration stops the app immediately instead of throwing when the options are first used.
When would you use IOptionsMonitor instead of IOptions?
Answer
When the value can change at runtime (reloaded configuration) or when a singleton needs the current value and change notifications.
How does an environment variable override Payments:BaseUrl?
Answer
Set Payments__BaseUrl; the double underscore stands for the colon section separator, and environment variables override appsettings files.
Open in the app to mark lessons complete, take notes and track your progress.