ASP.NET Core / Intermediate: Data, Identity, and Asynchronous Work / Lesson 7
Configuration, Options, and Secret Boundaries
Make provider precedence, typed settings, validation timing, reload behavior, and secret ownership explicit.
On this page
Lesson goals
- Explain ordered configuration providers and hierarchical keys.
- Validate typed options at the intended boundary.
- Keep settings and secrets out of accidental logs or test dependencies.
Configuration combines providers in a defined order
Configuration is a key/value abstraction built from providers such as JSON, environment, command line, or explicit memory data. Later providers can override earlier keys under their contracts. Host defaults are convenient but can make tests depend on machine state when they are used unintentionally. Record the actual providers and environment for reproducibility.
Hierarchical keys use a logical path, while each provider maps its source syntax to that path. Environment double underscores commonly represent separators, but this lesson does not read or change process environment. A missing key, empty value, malformed number, and secret access failure deserve different diagnostics.
Exercise precedence with invented memory providers
The first program adds two in-memory providers for one key. The second value wins, producing 6. This tests provider precedence without reading a repositoryโs appsettings, environment variables, user secrets, or external vault. A test should construct only the sources relevant to the behavior it claims to verify.
Do not infer that overriding a string makes every component reload a typed value immediately. Consumption through options, a captured singleton field, or a manually cached value can have different lifetimes. Provider change notification and application reaction are separate parts of a reload contract.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
using var configuration = new ConfigurationManager();
configuration.AddInMemoryCollection(new Dictionary<string, string?> { ["Reports:Limit"] = "4" });
configuration.AddInMemoryCollection(new Dictionary<string, string?> { ["Reports:Limit"] = "6" });
Console.WriteLine($"limit={configuration["Reports:Limit"]}");
limit=6
Only two synthetic providers participate; the later value is 6.
Typed options require an explicit validation time
Options group related settings behind a typed contract. IOptions, IOptionsSnapshot, and IOptionsMonitor have different caching, scope, and change-observation roles. Choose the form matching the consumer lifetime; do not inject a scoped snapshot into a singleton merely because it exposes the desired property.
Validation can run when an options value is created/accessed, while ValidateOnStart arranges startup validation through the host path. A console test reading IOptions.Value proves value validation at access, not host startup. Cross-field constraints and bounds should be explicit so a configuration typo does not become unbounded work or an insecure default.
Read validated options through the container
The second program registers one synthetic options value and a bounded page-size rule, then reads it through DI. A value outside the rule would produce the documented options-validation failure at creation/access. The displayed valid 25 is not a universal application limit. It is a local example of turning a domain requirement into a check.
Do not log the entire options object when it may contain connection secrets or tokens. Log a safe category and key name rather than credential contents. Treat failed startup validation as a clean refusal to accept work, not an excuse to continue using partially configured resources.
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
var services = new ServiceCollection();
services.AddOptions<ReportOptions>()
.Configure(options => options.PageSize = 25)
.Validate(options => options.PageSize is >= 1 and <= 100, "PageSize outside range");
using var provider = services.BuildServiceProvider();
int size = provider.GetRequiredService<IOptions<ReportOptions>>().Value.PageSize;
Console.WriteLine($"pageSize={size}");
internal sealed class ReportOptions { public int PageSize { get; set; } }
pageSize=25
Reading Value creates/validates the configured options; this is not a host-start test.
Configuration changes are application changes
Reloadable settings can alter limits, routes to dependencies, feature behavior, or security assumptions. Define whether a component captures a snapshot for one operation or observes changes between operations. Validate reloaded values and consider concurrency and cleanup when a setting replaces a resource. A notification alone does not safely rebuild every dependent object.
Record sources, defaults, required keys, secret delivery/rotation, validation, reload expectations, and deployment overrides. Tests should cover missing, malformed, out-of-range, overridden, and changed values under isolated sources. This course never reads configured secrets or changes application/global configuration.
Common errors and fixes
A test unexpectedly uses a machine environment value.
- Why it happens
- Host defaults read sources outside the intended fixture.
- How to fix it
- Construct isolated explicit providers for that test.
- Prevent it next time
- Record source order and overrides.
Valid options are assumed to reload a singleton automatically.
- Why it happens
- Provider notification and consumer lifetime were conflated.
- How to fix it
- Choose the proper options abstraction and reaction policy.
- Prevent it next time
- Test captured versus observed values.
A validation failure dumps secrets into logs.
- Why it happens
- The full configuration object is used as a diagnostic.
- How to fix it
- Log safe key/category context and redact sensitive values.
- Prevent it next time
- Review error paths with synthetic secret-shaped input.
Check your understanding
1. A singleton depends on IOptionsSnapshot<T>. Why is this a lifetime concern?
Show worked solution
Snapshot options are scoped, while the singleton lives for the application/container lifetime. Capturing them can violate intended scope and hide reload behavior. Choose a compatible options abstraction or an explicit operation scope, then test when values are read and updated. A type exposing the right property is not sufficient evidence that its lifetime fits the consumer.
2. What is the difference between testing IOptions.Value and testing ValidateOnStart?
Show worked solution
Reading Value can create and validate an options instance through DI. ValidateOnStart is integrated with host startup and needs that path to be exercised. Keep component validation tests for the rules, and add an isolated startup test for the public startup-failure contract when authorized. Do not report a direct options read as proof that an application refuses to listen with invalid configuration.
Key takeaways
- Provider order, typed consumption, validation, and reload are separate contracts.
- Options abstractions must match consumer lifetimes.
- Configuration error handling must not expose secrets or partially start work.