Configuration and the Options Pattern

Every real application needs settings — connection strings, API keys, feature flags, timeouts. ASP.NET Core gives you a powerful, layered configuration system through IConfiguration , and the Options pattern to turn raw settings into strongly typed classes you can inject and validate. This is the standard, idiomatic way to manage configuration in modern .NET.

Learn Configuration and the Options Pattern in our free C# course — an interactive lesson with worked examples, a practice exercise and a quick reference.

Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Think of configuration like the settings on a phone that arrive in layers . The manufacturer ships sensible defaults ( appsettings.json ), your carrier tweaks a few ( appsettings.Production.json ), and finally you override what you like in Settings (environment variables). Each layer can override the one before, and the value you actually see is the result of all layers merged. The Options pattern is then like a tidy "Wi-Fi settings" screen: instead of hunting through one giant list of keys, related settings are grouped into a clean, typed object.

IConfiguration is a single read-only key/value view, built by stacking providers . The Options pattern then binds a slice of that view to a class so your code never touches raw string keys.

Keys use a colon to express hierarchy ( Smtp:Host ); the same key as an environment variable uses a double underscore ( Smtp__Host ) because some shells dislike colons.

1. Reading IConfiguration

At its simplest, IConfiguration is a dictionary you read by key. Hierarchy is expressed with a colon, so Smtp:Host reaches into the Smtp section. Use the indexer for strings, or GetValue T to parse a typed value. Run the worked example to see it in action.

2. Layered Sources & Overrides

The real power is layering . You stack providers in order — JSON file, then environment-specific JSON, then user secrets, then environment variables, then command line — and later providers override earlier ones for the same key. This is how the same code runs in dev, staging, and production with different values.

Your turn. Bind the Smtp section onto a POCO by naming the section. Fill in the ___ blank.

3. The Options Pattern: Configure T + IOptions T

Rather than reading keys by hand, register a binding once with services.Configure T (config.GetSection("...")) . Then any service can inject IOptions T and read .Value to get a clean, strongly typed settings object. This decouples your code from raw configuration keys.

Now you try: register the options and read the bound value. Fill in the two ___ blanks.

4. Validating Options (Fail Fast)

Bad configuration should break the app at startup , not three layers deep at 2 a.m. Using AddOptions T ().Bind(...).ValidateDataAnnotations().ValidateOnStart() , you can decorate your POCO with [Required] and [Range] and have the app refuse to start if the config is invalid.

The Options pattern offers three consumption interfaces, and picking the right one matters:

Rule of thumb: reach for IOptions T by default. Use IOptionsSnapshot T in request-scoped services that should see per-request changes, and IOptionsMonitor T in long-lived singletons (background services) that need to notice config file changes while running:

Here's the idiomatic end-to-end flow: a POCO describes the settings, Configure T binds the section, and a service depends on IOptions T rather than on raw configuration. The service has no idea where the values came from — file, env var, or secret — and that's exactly the point.

Swap the config source and the Notifier code never changes — it just consumes a clean settings object.

Q: If a key is in both the JSON file and an environment variable, which wins?

The environment variable, normally. Sources are layered in order and later providers override earlier ones; environment variables are added after the JSON files, so they win for the same key.

Q: When should I use IOptionsSnapshot T vs IOptionsMonitor T ?

IOptionsSnapshot T is scoped — great for getting fresh values per web request. IOptionsMonitor T is a singleton with CurrentValue and OnChange — use it in long-lived singletons that must react to config reloads.

Q: Where do I keep secrets during development?

In the Secret Manager (user secrets), which stores them outside your project folder and out of source control. In production use environment variables or a secrets vault — never appsettings.json .

Q: Why bother binding to a class instead of reading keys directly?

Typed options give you compile-time safety, support validation, and decouple your services from raw string keys. Consumers depend on a clean settings object, and the shape lives in one place.

No blanks this time — just a brief and an outline. Define a PaymentOptions POCO, build configuration with a Payment section, register it with Configure T , then resolve IOptions PaymentOptions and print the provider and fee. Run it and check your output against the expected line.

Practice quiz

What is IConfiguration in ASP.NET Core?

  • A unified, read-only view of settings merged from many sources
  • A logging framework
  • A database connection
  • A dependency injection container

Answer: A unified, read-only view of settings merged from many sources. IConfiguration presents a single key/value view built by layering multiple configuration sources (files, env vars, secrets) into one read-only model.

If the same key appears in appsettings.json and in an environment variable, which value usually wins?

  • It throws an error
  • Whichever is alphabetically first
  • The environment variable, because later sources override earlier ones
  • appsettings.json always wins

Answer: The environment variable, because later sources override earlier ones. Configuration sources are layered in order; later providers override earlier ones. Environment variables are typically added after JSON files, so they override them.

What does the Options pattern let you do?

  • Only read environment variables
  • Bind a configuration section to a strongly typed POCO class
  • Replace dependency injection
  • Disable configuration entirely

Answer: Bind a configuration section to a strongly typed POCO class. The Options pattern binds a section of configuration to a plain class (POCO) so your code consumes strongly typed settings instead of raw string keys.

Which call registers a settings class to bind from a configuration section?

  • config.Bind()
  • services.AddSingleton<T>()
  • services.UseOptions<T>()
  • services.Configure<T>(config.GetSection("..."))

Answer: services.Configure<T>(config.GetSection("...")). services.Configure<T>(configuration.GetSection("Name")) registers the binding so T can be injected later as IOptions<T> and friends.

What is the difference between IOptions<T> and IOptionsSnapshot<T>?

  • IOptions<T> is a singleton computed once; IOptionsSnapshot<T> is scoped and re-reads per request
  • They are identical
  • IOptionsSnapshot<T> is never recomputed
  • IOptions<T> only works in console apps

Answer: IOptions<T> is a singleton computed once; IOptionsSnapshot<T> is scoped and re-reads per request. IOptions<T> is a singleton bound once at startup. IOptionsSnapshot<T> is scoped and recomputed per request, so it can pick up changes for each request.

Which interface is best for a singleton service that must react to config changes at runtime?

  • IOptionsSnapshot<T>
  • IOptionsMonitor<T>
  • IConfiguration directly
  • IOptions<T>

Answer: IOptionsMonitor<T>. IOptionsMonitor<T> exposes CurrentValue and an OnChange callback, so a singleton can read fresh values and respond to reloads at runtime.

What is the recommended place to store secrets like API keys during local development?

  • Hard-coded in the source
  • In a public README
  • Committed in appsettings.json
  • The Secret Manager (user secrets), kept out of source control

Answer: The Secret Manager (user secrets), kept out of source control. The user secrets store (Secret Manager) keeps development secrets outside the project folder and out of source control, unlike appsettings.json.

How are nested settings like Logging:LogLevel:Default referenced in configuration keys?

  • With a slash
  • They cannot be nested
  • With a colon separator, Logging:LogLevel:Default
  • With a dot, Logging.LogLevel.Default

Answer: With a colon separator, Logging:LogLevel:Default. Configuration keys use a colon to denote hierarchy; environment variables use a double underscore that maps to the colon.

How can you validate bound options so a bad config fails fast?

  • You cannot validate options
  • Use ValidateDataAnnotations() and ValidateOnStart() on the OptionsBuilder
  • Only by manual if-checks in every controller
  • By disabling configuration

Answer: Use ValidateDataAnnotations() and ValidateOnStart() on the OptionsBuilder. services.AddOptions<T>().Bind(...).ValidateDataAnnotations().ValidateOnStart() validates the bound values and fails at startup if they're invalid.

Why prefer the Options pattern over reading IConfiguration string keys everywhere?

  • It gives strong typing, validation, and decouples code from raw keys
  • It is slower but simpler
  • It avoids using dependency injection
  • It only works for connection strings

Answer: It gives strong typing, validation, and decouples code from raw keys. Binding to typed classes gives compile-time safety, supports validation, and means consumers depend on a clean settings object rather than scattered magic strings.