Configuration starts simply in ASP.NET Core. We add a few values to appsettings.json, inject IConfiguration, and move on. But as applications grow across services, environments, regions, and deployment platforms, configuration becomes part of the architecture itself. A single incorrect setting can disable a feature, overwhelm a dependency, or prevent an application from starting. Modern ASP.NET Core gives us powerful tools for strongly typed settings, validation, environment-specific configuration, and runtime updates. The challenge is using those tools safely.
In this guide, we’ll build a scalable configuration approach that validates settings early, supports controlled changes, and allows applications to react to updated configuration without introducing unpredictable production behavior.
Configuration Looks Easy Until the Application Grows
Consider a small ASP.NET Core application.
Its configuration might contain:
{
"Catalog": {
"PageSize": 25
}
}Reading the value is straightforward.
var pageSize =
builder.Configuration.GetValue<int>("Catalog:PageSize");For a small application, this works perfectly well.
Now imagine the same system two years later.
It contains:
Multiple ASP.NET Core services
Development, staging, and production environments
Several geographic regions
Feature settings
Retry policies
API endpoints
Cache expiration rules
Queue settings
Connection information
Operational limits
Suddenly, configuration is no longer just a file.
It is a distributed operational dependency.
Why Configuration Deserves Architectural Attention
A configuration change can alter application behavior without changing application code.
That is powerful.
It is also dangerous.
Imagine someone changes:
{
"Payments": {
"TimeoutSeconds": 30
}
}to:
{
"Payments": {
"TimeoutSeconds": 3000
}
}The application still compiles.
The deployment may succeed.
But requests could now remain active far longer than intended.
Configuration errors are particularly difficult because they often look like application failures.
The code has not changed, yet production behavior has.
That is why mature systems treat configuration with many of the same disciplines applied to code.
Understanding ASP.NET Core Configuration Providers
ASP.NET Core builds configuration from one or more configuration providers.
Common sources include:
JSON files
Environment variables
Command-line arguments
User secrets during development
In-memory configuration
External configuration systems
These sources are combined into a single configuration model.
A simplified setup might look like:
var builder = WebApplication.CreateBuilder(args);
builder.Configuration
.AddJsonFile(
"appsettings.json",
optional: false,
reloadOnChange: true)
.AddJsonFile(
$"appsettings.{builder.Environment.EnvironmentName}.json",
optional: true,
reloadOnChange: true)
.AddEnvironmentVariables();The order matters because later providers can override values supplied by earlier providers.
That gives us a useful layering model.
Base Configuration
↓
Environment Configuration
↓
Environment Variables
↓
Final Effective ConfigurationThe application consumes the final result.
Configuration Precedence Matters
Suppose appsettings.json contains:
{
"Catalog": {
"PageSize": 20
}
}Production configuration contains:
{
"Catalog": {
"PageSize": 50
}
}And an environment variable supplies:
Catalog__PageSize=100The effective value may become:
100That flexibility is useful for deployments, but it can also create confusion.
When debugging configuration problems, the important question is often not:
What does appsettings.json say?
It is:
Which provider supplied the value the application is actually using?
Stop Passing IConfiguration Everywhere
A common early-stage pattern looks like this:
public class CatalogService
{
private readonly IConfiguration _configuration;
public CatalogService(IConfiguration configuration)
{
_configuration = configuration;
}
public int GetPageSize()
{
return _configuration
.GetValue<int>("Catalog:PageSize");
}
}This works.
But it has several weaknesses.
The service depends on string-based configuration paths.
Typos become runtime problems.
The expected configuration structure is hidden inside application logic.
Validation is difficult.
Testing requires constructing configuration.
For larger applications, the Options pattern is usually a better approach.
Strongly Typed Configuration
Let’s define configuration as a class.
public sealed class CatalogOptions
{
public const string SectionName = "Catalog";
public int PageSize { get; init; }
public int CacheMinutes { get; init; }
}Our configuration becomes:
{
"Catalog": {
"PageSize": 50,
"CacheMinutes": 10
}
}Then register it:
builder.Services.Configure<CatalogOptions>(
builder.Configuration.GetSection(
CatalogOptions.SectionName));Now application code works with normal C# properties rather than configuration strings.
public class CatalogService
{
private readonly CatalogOptions _options;
public CatalogService(
IOptions<CatalogOptions> options)
{
_options = options.Value;
}
}This gives configuration a clear contract.
Why Strong Typing Matters at Scale
Compare these:
configuration["Payments:Retry:MaximumAttempts"]and:
options.MaximumAttemptsThe second approach is easier to understand, test, refactor, and validate.
More importantly, it tells developers exactly which settings a component expects.
Configuration stops being an invisible collection of strings and becomes part of the application’s type system.
Configuration Must Be Validated
Strong typing does not automatically make configuration correct.
This is perfectly valid C#:
{
"Catalog": {
"PageSize": -500,
"CacheMinutes": 0
}
}The values bind successfully.
But they make no business sense.
ASP.NET Core’s options infrastructure allows us to validate configuration.
For example:
builder.Services
.AddOptions<CatalogOptions>()
.Bind(
builder.Configuration.GetSection(
CatalogOptions.SectionName))
.Validate(
options => options.PageSize > 0,
"PageSize must be greater than zero.")
.Validate(
options => options.CacheMinutes > 0,
"CacheMinutes must be greater than zero.");Now invalid values can be detected deliberately rather than producing strange behavior later.
Fail Fast with ValidateOnStart
There is another important question.
When should invalid configuration be discovered?
Suppose a payment configuration is invalid.
One option is to discover the problem when the first customer attempts a payment.
That is far too late.
For critical configuration, we usually want the application to fail during startup.
builder.Services
.AddOptions<PaymentOptions>()
.Bind(
builder.Configuration.GetSection("Payments"))
.Validate(
options => options.TimeoutSeconds > 0,
"Payment timeout must be positive.")
.ValidateOnStart();Now configuration is checked when the application starts.
This is an important production principle:
A service that cannot operate correctly should fail before it starts accepting traffic.
Data Annotation Validation
For straightforward rules, data annotations can make option classes easy to understand.
public sealed class PaymentOptions
{
[Range(1, 120)]
public int TimeoutSeconds { get; init; }
[Range(0, 10)]
public int MaximumRetries { get; init; }
}Then:
builder.Services
.AddOptions<PaymentOptions>()
.BindConfiguration("Payments")
.ValidateDataAnnotations()
.ValidateOnStart();The rules now live directly beside the properties they protect.
Cross-Property Validation
Real configuration often has relationships between values.
Imagine:
public sealed class UploadOptions
{
public long WarningSizeBytes { get; init; }
public long MaximumSizeBytes { get; init; }
}Both numbers could individually be positive while still being logically incorrect.
For example:
WarningSize = 100 MB
MaximumSize = 50 MBWe need a rule such as:
.Validate(
options =>
options.WarningSizeBytes <
options.MaximumSizeBytes,
"WarningSizeBytes must be lower than MaximumSizeBytes.")This is why configuration validation should express operational rules, not merely type correctness.
IOptions, IOptionsSnapshot, and IOptionsMonitor
ASP.NET Core provides several ways to consume options, and understanding the differences becomes important when configuration can change.
The three common interfaces are:
IOptions<T>
IOptionsSnapshot<T>
IOptionsMonitor<T>They sound similar, but they serve different purposes.
IOptions<T>
IOptions<T> provides an options value that is effectively consumed as a stable value by the application.
It works well when configuration is not expected to change during the process lifetime.
public CatalogService(
IOptions<CatalogOptions> options)
{
_options = options.Value;
}For many settings, this is exactly what we want.
Database architecture, protocol choices, and other structural settings should not necessarily change while the application is running.
IOptionsSnapshot<T>
IOptionsSnapshot<T> is scoped and recalculates options once per request scope.
This can be useful in request-based applications where updated configuration should become visible to subsequent requests.
public CatalogController(
IOptionsSnapshot<CatalogOptions> options)
{
_options = options.Value;
}A request sees a consistent snapshot.
A later request can receive updated settings.
IOptionsMonitor<T>
IOptionsMonitor<T> is designed for scenarios where an application needs access to the current option value and may need to react when that value changes.
public class PricingService
{
private readonly IOptionsMonitor<PricingOptions> _options;
public PricingService(
IOptionsMonitor<PricingOptions> options)
{
_options = options;
}
public decimal Calculate(decimal price)
{
var settings = _options.CurrentValue;
return price * settings.Multiplier;
}
}If the underlying configuration provider supports reloads, CurrentValue can reflect updated configuration.
Reacting to Configuration Changes
IOptionsMonitor<T> also exposes change notifications.
public class PricingConfigurationObserver
{
private readonly IDisposable? _subscription;
public PricingConfigurationObserver(
IOptionsMonitor<PricingOptions> options,
ILogger<PricingConfigurationObserver> logger)
{
_subscription = options.OnChange(updated =>
{
logger.LogInformation(
"Pricing configuration changed. Multiplier: {Multiplier}",
updated.Multiplier);
});
}
}This looks extremely powerful.
Change a configuration value and the application reacts immediately.
But this is also where configuration becomes dangerous.
Dynamic Configuration Is Not Automatically Better
Imagine changing:
MaximumConcurrentJobs = 100to:
MaximumConcurrentJobs = 10while 75 jobs are already running.
What should happen?
Should 65 jobs be cancelled?
Should the new limit apply only to future work?
Should the application wait until concurrency naturally falls below 10?
The configuration system cannot answer those questions.
They are application semantics.
This leads to one of the most important rules in dynamic configuration:
Being able to reload a setting does not mean the setting is safe to reload.
Classify Configuration by Behavior
A useful production approach is to classify settings into three groups.
Static Settings
These require application restart.
Examples:
Fundamental infrastructure choices
Certain hosting settings
Major dependency topology
Settings used only during service registration
Reloadable Settings
These can change safely while the application is running.
Examples might include:
Some timeout values
UI behavior
Operational thresholds
Non-critical feature behavior
Controlled Dynamic Settings
These can change at runtime, but only through explicit application logic.
Examples include:
Concurrency limits
Pricing rules
Traffic percentages
Resource thresholds
This classification prevents teams from treating every configuration value as equally dynamic.
Safe Reloading Requires Boundaries
Suppose we have:
public sealed class WorkerOptions
{
public int BatchSize { get; init; }
}A background worker could read:
var batchSize =
_optionsMonitor.CurrentValue.BatchSize;before beginning each batch.
That gives us a natural boundary.
Batch 1 uses 100
↓
Configuration changes to 50
↓
Batch 2 uses 50The current batch remains consistent.
The new value applies to future work.
That is far safer than changing behavior halfway through an operation.
Configuration Should Be Immutable During an Operation
Consider a request that performs several calculations.
If configuration can change halfway through the request, we could theoretically get:
Step 1 → Tax rate 10%
Step 2 → configuration reload
Step 3 → Tax rate 12%One operation has now used two different business rules.
For important calculations, capture the required configuration once at the start of the operation.
Then use that snapshot consistently.
Dynamic configuration should change between operations, not unpredictably inside them.
What Happens When Reloaded Configuration Is Invalid?
This is where production systems need careful design.
Suppose:
{
"Worker": {
"BatchSize": 100
}
}changes to:
{
"Worker": {
"BatchSize": -10
}
}The new value is invalid.
A mature system should not blindly adopt it.
Validation needs to remain part of the options pipeline, and operationally you should also make configuration failures visible through logging and monitoring.
The goal is simple:
New Configuration
↓
Validation
/ \
Valid Invalid
↓ ↓
Use it Surface failureNever make “the file changed” equivalent to “the change is safe.”
External Configuration Systems
Once an organization runs many application instances, local JSON files become difficult to manage dynamically.
Imagine 50 instances of the same service.
Changing a JSON file manually on each server is obviously not a scalable strategy.
This is where centralized configuration providers can help.
Depending on the environment, organizations may use systems such as:
Azure App Configuration
Cloud configuration services
Kubernetes configuration mechanisms
Dedicated internal configuration platforms
The architectural pattern becomes:
Central Configuration
↓
Application A
Application B
Application C
Application DTeams can manage settings centrally rather than maintaining independent copies everywhere.
Configuration Is Not the Same as Secrets
This distinction matters.
Configuration might contain:
PageSize = 50
RetryCount = 3
FeatureMode = StandardSecrets include things such as:
Database password
API credential
Private key
Signing secretBoth may enter an application through configuration providers, but they should not necessarily be managed in the same place or with the same access policies.
Secrets deserve stronger controls, auditing, rotation, and restricted visibility.
Do not turn appsettings.json into a password vault.
Environment Variables
Environment variables remain extremely useful for containerized and cloud deployments.
Nested configuration keys use double underscores.
For example:
Payments__TimeoutSeconds=20maps to:
Payments:TimeoutSecondsThis allows deployment platforms to override application defaults without modifying files inside the application package.
Environment-Specific Files
ASP.NET Core commonly uses files such as:
appsettings.json
appsettings.Development.json
appsettings.Production.jsonThis is useful for environment-specific defaults.
But avoid allowing these files to drift into completely different application configurations.
If production contains 40 settings that development does not even know exist, configuration becomes difficult to test.
The environments should differ in values, not unexpectedly in the fundamental shape of configuration.
Validate Production-Like Configuration Before Production
Configuration validation during application startup is valuable.
But discovering an invalid setting during the production deployment is still later than ideal.
A stronger delivery pipeline validates production configuration earlier.
Conceptually:
Configuration Change
↓
Schema / Options Validation
↓
Automated Tests
↓
Staging
↓
ProductionTreat configuration changes as deployable changes.
They deserve review and testing.
Configuration Versioning
Imagine an incident begins at 14:32.
The code has been running successfully for three days.
What changed?
A mature configuration system should make it possible to answer:
What changed?
Who changed it?
When?
From what value?
To what value?
Which services received it?This is why version history becomes important at scale.
Configuration without history makes incident investigation much harder.
Rollback Matters
If configuration can be changed quickly, it should also be possible to reverse the change quickly.
Suppose a new timeout causes failures.
A rollback path should look like:
Version 41
↓
Version 42
↓
Problems detected
↓
Rollback
↓
Version 41The ability to restore a known-good configuration is often more valuable during an incident than the ability to edit individual values manually.
Avoid Giant Global Configuration Objects
Another common mistake is creating:
public class ApplicationSettings
{
// 150 unrelated properties
}Then every service depends on it.
This creates unnecessary coupling.
Instead, define configuration around capabilities.
PaymentOptions
CatalogOptions
CacheOptions
ShippingOptions
NotificationOptionsEach component receives only the configuration it needs.
This mirrors the same modularity principles we apply to application architecture.
Names Matter
Avoid settings such as:
Timeout = 30
Limit = 100
Enabled = trueThirty what?
Seconds?
Milliseconds?
What limit?
What exactly is enabled?
Prefer explicit names:
RequestTimeoutSeconds
MaximumConcurrentRequests
EnableAutomaticRetryConfiguration is an interface between developers and operators.
Clarity matters.
Do Not Use Configuration as a Database
Once dynamic configuration becomes convenient, teams sometimes begin putting everything into it.
That is a mistake.
Configuration is excellent for controlling application behavior.
It is not a replacement for transactional business data.
A customer’s current account balance does not belong in configuration.
Neither does an order’s status.
Ask:
Is this describing how the application should behave, or is this information the application owns?
If it is business data, store it as business data.
Feature Flags Are a Separate Concern
Feature flags and configuration overlap, but they solve different problems.
Configuration usually describes operational behavior.
Feature flags control whether particular functionality is available, often for specific users, tenants, or rollout groups.
For example:
RequestTimeoutSeconds = 20is configuration.
EnableNewCheckoutForBetaCustomers = trueis closer to feature management.
Keeping that distinction clear prevents configuration systems from becoming overloaded with rollout logic.
Observing Configuration Changes
Configuration changes should produce operational signals.
At minimum, important changes should be logged.
For example:
logger.LogInformation(
"Catalog configuration updated. PageSize={PageSize}",
options.PageSize);Be careful not to log secrets.
For important systems, useful telemetry can include:
Configuration reload count
Validation failures
Last successful refresh
Configuration version
Refresh latency
Configuration is an operational dependency, so it deserves observability.
Health Checks and Configuration
Some configuration failures can also affect service health.
Suppose an application depends on centrally refreshed configuration and has not successfully refreshed for several hours.
Whether that should make the service unhealthy depends on the system.
Do not automatically mark a service unhealthy simply because a configuration provider is temporarily unavailable if the application can safely continue using its last known configuration.
This is an important resilience principle:
A temporary inability to refresh configuration should not necessarily destroy an otherwise healthy application.
Last Known Good Configuration
That leads to another useful pattern.
Imagine the configuration service becomes unavailable.
The application already has a valid configuration.
Should it stop working?
Often, no.
A resilient architecture can continue using the last known good configuration while reporting that refresh attempts are failing.
Configuration Provider
X
│
Application
│
└── Continue with last known good settingsThis prevents a configuration outage from automatically becoming an application outage.
Of course, some security-sensitive settings may require stricter behavior.
Again, the correct policy depends on what the setting controls.
Configuration Across Multiple Instances
Suppose we run ten instances of an Orders API.
A configuration value changes.
The instances may not all observe the update at exactly the same instant.
For a brief period:
Instance 1 → Version 18
Instance 2 → Version 18
Instance 3 → Version 17
Instance 4 → Version 18Can the system tolerate that?
For many operational settings, yes.
For some business rules, absolutely not.
If every instance must change atomically, ordinary dynamic configuration may be the wrong mechanism.
This is another reason to distinguish operational settings from transactional business state.
Configuration and .NET Aspire
In our previous article, we explored how .NET Aspire helps describe and connect distributed applications.
Configuration fits naturally into that story.
Aspire can help applications receive connection information and resource references without requiring developers to manually coordinate endpoints across multiple projects.
The larger architectural principle remains the same:
Applications should depend on named capabilities and structured configuration rather than scattered hardcoded infrastructure details.
As distributed systems grow, this becomes increasingly important.
A Practical Production Pattern
Imagine an ASP.NET Core order-processing service.
It needs:
OrderProcessingOptions
PaymentOptions
ShippingOptions
QueueOptionsEach option type is:
Strongly typed.
Bound to its own configuration section.
Validated.
Validated during startup when appropriate.
Classified as static or reloadable.
Monitored when dynamic changes are required.
Critical settings fail startup if invalid.
Reloadable settings change only at safe operational boundaries.
Configuration changes are logged.
The configuration platform maintains version history.
Rollbacks are possible.
Secrets are managed separately.
This is no longer “reading settings.”
It is configuration engineering.
A Real-World Scenario
Imagine a large ticket-booking platform.
During normal traffic:
MaximumSearchResults = 100
SearchTimeoutSeconds = 5
MaximumConcurrentSearches = 500
CacheMinutes = 10A major concert goes on sale.
Traffic suddenly increases dramatically.
Operations decides to reduce:
MaximumSearchResultsfrom:
100to:
50The goal is to reduce backend work per search.
A properly designed configuration system allows the change to be introduced without rebuilding the application.
New requests use the updated limit.
Existing requests continue using the configuration they started with.
Telemetry confirms the new configuration version has reached application instances.
Latency begins falling.
If the change causes unexpected behavior, operations rolls back to the previous version.
That is the value of dynamic configuration when it is designed correctly.
The Configuration Maturity Ladder
We can think of configuration maturity in stages.
Level 1
Hardcoded values
↓
Level 2
appsettings.json
↓
Level 3
Environment-specific configuration
↓
Level 4
Strongly typed options
↓
Level 5
Validation
↓
Level 6
Centralized configuration
↓
Level 7
Safe dynamic reload
↓
Level 8
Versioning, auditing, rollback, and observabilityNot every application needs Level 8.
But as systems become more distributed and operationally important, configuration deserves increasingly deliberate engineering.
Common Mistakes to Avoid
The most common problems usually come from treating configuration as an afterthought.
Avoid:
Hardcoded infrastructure addresses
Configuration strings scattered throughout application code
Missing validation
Secrets committed to configuration files
Reloading every setting dynamically
Changing values halfway through operations
Giant global settings classes
Unclear units and names
No version history
No rollback mechanism
Logging secret values
Assuming every instance reloads simultaneously
Most of these mistakes are easy to avoid when configuration is treated as an explicit architectural concern.
How This Fits Our ASP.NET Core Journey
The previous article introduced .NET Aspire and showed how distributed applications can be modeled as connected resources.
That naturally leads to configuration.
Once applications span multiple services, environments, and infrastructure dependencies, every component needs reliable information about how it should behave.
The challenge is no longer simply loading that information.
The challenge becomes controlling its lifecycle.
We need to know whether configuration is valid, when it can change, how changes reach running services, what happens when the provider fails, and how we recover when a bad value reaches production.
Those questions become increasingly important as ASP.NET Core systems grow.
And they lead directly into our next architectural problem.
What happens when a perfectly configured application receives more work than it can possibly process?
That is where backpressure and overload protection enter the picture.
Coming Next
In the next article, we’ll explore Designing Backpressure in ASP.NET Core: Handling Overload Without Crashing Your System.
We’ll look at what happens when producers generate work faster than consumers can process it, why unlimited queues can quietly destroy application stability, and how bounded channels, concurrency limits, load shedding, cancellation, and graceful degradation help ASP.NET Core applications remain responsive under extreme load.
Closing Thoughts
Configuration begins as a convenience and eventually becomes part of the operational architecture of an application.
ASP.NET Core gives us a strong foundation through configuration providers, the Options pattern, validation, startup validation, snapshots, and monitoring. But the framework cannot decide which settings are safe to change while an application is running.
That responsibility belongs to us.
The most reliable systems distinguish static configuration from genuinely dynamic settings. They validate important values before accepting traffic, apply runtime changes at safe boundaries, preserve known-good settings when appropriate, record configuration history, and provide a fast rollback path when something goes wrong.
At scale, configuration should not be a collection of mysterious values scattered across files and environment variables.
It should be a controlled, validated, observable input into application behavior.
When we treat it that way, configuration stops being one of the easiest ways to accidentally break production and becomes one of the safest ways to operate a system without constantly redeploying it.
Subscribe Now
Enjoying the series? Subscribe to ASP Today for practical ASP.NET Core tutorials, advanced architecture deep dives, and production-focused .NET engineering guides. Join our Substack Chat to discuss configuration strategies, distributed systems, and the challenges of running modern ASP.NET Core applications at scale.


