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.

Create a free account to save this page, take notes and track your progress. Sign in

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() and Validate(...) 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) or IOptionsMonitor<T> (current value plus change notifications, usable from singletons).

The code

Options validated at startup
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 budget

Run 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); use IOptionsMonitor<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.

Was this page helpful?