← Open in the full interactive course (progress tracking, search & more)

Your app's settings shouldn't be hardcoded — they should come from outside, layered and overridable.

Your app needs a connection string. Where does it come from? If you write const string ConnectionString = "Server=prod-db;..."; directly in your code, you've made a decision you'll regret the first time you need to run against a different database — for testing, for staging, or for a teammate's local machine. Now you're editing source code and recompiling just to point at a different server.

.NET's configuration system solves this by treating settings as data that flows in from outside your code — files, environment variables, command-line arguments — and lets you read it all through one unified interface, with a predictable, well-defined order for which source wins when two disagree.

In this lesson, you'll learn how IConfiguration works, how appsettings.json fits in, what configuration providers are and their override order, and how to bind a configuration section into a strongly-typed class.

What Is It?

The Simple Explanation

Configuration is all the settings your app needs to run that shouldn't be baked into the compiled code — connection strings, API keys, feature flags, retry counts. IConfiguration is the single .NET interface your code uses to read those settings, no matter where they actually came from.

The Technical Definition

IConfiguration represents a set of key/value application configuration properties, built from one or more configuration providers — sources like JSON files, environment variables, or command-line arguments — merged together into a single, hierarchical view. Keys can be nested using a colon (Logging:LogLevel:Default) or, when represented in JSON, ordinary nested objects.

appsettings.json

Environment variables

Why Does It Exist?

The Problem — Hardcoded Settings Don't Survive Contact With Reality

The Solution — Layered, External Configuration

.NET's configuration system lets you define sensible defaults in a checked-in JSON file, then override specific values per environment using environment variables or command-line arguments — without touching a single line of code or recompiling. The same compiled binary runs correctly in every environment, because the values it reads change, not the code itself.

Big Picture

THE PROVIDER STACK (LATER SOURCES WIN)
appsettings.json               ← base defaults
        ↓ overridden by
appsettings.{Environment}.json ← environment-specific overrides
        ↓ overridden by
Environment Variables          ← deploy-time / secret overrides
        ↓ overridden by
Command-Line Arguments         ← highest priority, wins over everything

Each provider is layered on top of the ones before it. If two providers define the same key, the one added later wins. This is the entire mental model you need for 90% of configuration questions: "which source was added last?"

How It Works

FROM appsettings.json TO IConfiguration
1. HOST.CREATEAPPLICATIONBUILDER LOADS DEFAULT PROVIDERS
var builder = Host.CreateApplicationBuilder(args);
2. READ VALUES THROUGH IConfiguration
string? connString = builder.Configuration["ConnectionStrings:Default"];
string? retries = builder.Configuration.GetSection("Api")["RetryCount"];
3. BIND A SECTION TO A STRONGLY-TYPED CLASS
var apiSettings = new ApiSettings();
builder.Configuration.GetSection("Api").Bind(apiSettings);

Simple Example

appsettings.json:

{
  "Api": {
    "BaseUrl": "https://api.example.com",
    "RetryCount": 3,
    "TimeoutSeconds": 30
  }
}

A matching, strongly-typed class:

public class ApiSettings
{
    public string BaseUrl { get; set; } = string.Empty;
    public int RetryCount { get; set; }
    public int TimeoutSeconds { get; set; }
}

var builder = Host.CreateApplicationBuilder(args);

var apiSettings = new ApiSettings();
builder.Configuration.GetSection("Api").Bind(apiSettings);

Console.WriteLine(apiSettings.BaseUrl);     // https://api.example.com
Console.WriteLine(apiSettings.RetryCount);  // 3

Now run the app with an environment variable override — no code change, no rebuild:

# Environment variables use double-underscore for nesting instead of colon
export Api__RetryCount=5

dotnet run
# apiSettings.RetryCount is now 5 — env var overrode the JSON default

Note the double underscore (__) — most shells and operating systems don't allow colons in environment variable names, so the configuration system accepts __ as an equivalent separator specifically for environment variables.

Real-World Example

A notification service reads its SMTP settings from configuration, registers the bound settings as a Singleton, and injects them into the service that needs them:

// appsettings.json
// { "Smtp": { "Host": "localhost", "Port": 25, "FromAddress": "no-reply@shop.com" } }

public class SmtpSettings
{
    public string Host { get; set; } = string.Empty;
    public int Port { get; set; }
    public string FromAddress { get; set; } = string.Empty;
}

var builder = Host.CreateApplicationBuilder(args);

var smtpSettings = new SmtpSettings();
builder.Configuration.GetSection("Smtp").Bind(smtpSettings);
builder.Services.AddSingleton(smtpSettings);
builder.Services.AddSingleton<IEmailSender, SmtpEmailSender>();

public class SmtpEmailSender(SmtpSettings settings) : IEmailSender
{
    public void Send(string to, string message) =>
        Console.WriteLine($"[{settings.Host}:{settings.Port}] From {settings.FromAddress} → {to}: {message}");
}

In production, an ops engineer sets Smtp__Host and Smtp__Port as environment variables on the deployment platform — the checked-in appsettings.json keeps sensible local-development defaults, and nobody touches source code to change where email gets sent.

Note: Manually binding into a plain class like this works, but the Options Pattern (IOptions<T>, covered in lesson 127) is the standard, more powerful way to do this in real applications — it adds validation and reload support on top of exactly this binding mechanism.

Analogy

Sticky Notes on a Whiteboard

Imagine a whiteboard with a base layer of settings written in permanent marker (appsettings.json) — the defaults everyone starts with. Over that, someone sticks a note for "Staging" settings, covering a few of the permanent-marker values with new ones. Over that, someone pins an index card for "today's environment variables," covering a few more. Finally, someone can scribble a value directly on top of everything, in command-line arguments, which always shows through no matter what's underneath.

To read the current value of any setting, you just look at what's visible on top — you don't need to know or care what's hidden underneath it.

Under the Hood

HOW PROVIDER LAYERING ACTUALLY WORKS
1. EACH PROVIDER IS A SIMPLE KEY/VALUE DICTIONARY BUILDER
2. PROVIDERS ARE ADDED TO A LIST, IN ORDER
3. LOOKUP WALKS THE LIST FROM LAST TO FIRST

Common Confusion

"Override" means replace, not merge

If appsettings.json defines an entire "Api" object with three properties, and an environment variable only overrides Api__RetryCount, the other two properties (BaseUrl, TimeoutSeconds) are untouched — only the specific key that the environment variable targets is replaced. Overriding happens per-key, not per-object.

Configuration values are always strings under the hood

Even "RetryCount": 3 in JSON is stored internally as the string "3". When you Bind() to a typed class, the binder converts strings to the target property's type (int, bool, enum, etc.) for you — but if you read a raw value with the indexer (configuration["Api:RetryCount"]), you always get back a string?, not an int.

Common Mistakes

Mistake 1 — Committing real secrets into appsettings.json

Writing a production API key or database password directly into appsettings.json, which gets committed to source control.

Keep only non-sensitive defaults in appsettings.json. Supply real secrets via environment variables, a secret manager, or (locally) the .NET User Secrets tool — never commit them.

Mistake 2 — Using a colon in an environment variable name

Trying to set Api:RetryCount as an actual OS environment variable name — many shells reject colons in variable names, or silently mangle them.

Use double underscore for environment variables: Api__RetryCount. The configuration system automatically translates it to the colon-separated key internally.

Mistake 3 — Assuming the indexer returns a typed value

int retries = builder.Configuration["Api:RetryCount"]; — this doesn't compile; the indexer returns string?.

Either parse it explicitly (int.Parse(...)), use GetValue<int>("Api:RetryCount"), or (preferably, for anything beyond a single value) bind the whole section to a typed class.

When Should I Use It?

Mental Model

Configuration = settings that live outside your compiled code
Providers = the sources those settings come from, stacked in order
Last provider added, for a given key, wins

Remember:
· json → environment variables → command-line args, in increasing priority.
· Bind a section to a class for type-safe access instead of stringly-typed lookups everywhere.
· Configuration is for what varies by environment; secrets need extra protection beyond just "configuration."

Key Takeaway


Check Your Understanding

You've seen how configuration is layered from multiple providers. Let's check your grasp of the override order.

1. appsettings.json sets Api:RetryCount to 3. An environment variable Api__RetryCount=7 is also set. What value does IConfiguration report?

Show answer

Correct: B

Why B is correct: Environment variables are registered as a provider after the JSON files by default, so for any key they both define, the environment variable's value wins.

Why A is incorrect: It's backwards — later-added providers override earlier ones, and JSON is added first.

Why C is incorrect: Conflicting keys across providers is the normal, expected case — the system resolves it deterministically rather than erroring.

Why D is incorrect: Configuration values are never numerically merged — the later provider's value simply replaces the earlier one for that key.

Reinforcement: Override order: json → environment variables → command-line args.

2. Why does the environment variable provider use Api__RetryCount (double underscore) instead of Api:RetryCount (colon)?

Show answer

Correct: B

Why B is correct: Environment variable names generally can't contain colons on most platforms, so the configuration system's environment variable provider translates a double underscore into a colon internally, letting it represent the same nested-key structure.

Why A is incorrect: Typing speed has nothing to do with the design decision.

Why C is incorrect: Colons are used as the general internal key separator across configuration, not something reserved to command-line args specifically.

Why D is incorrect: It's a deliberate, documented convention specifically to work around environment variable naming restrictions.

Reinforcement: Colon in code, double underscore in environment variable names — same nested key, different syntax per source.

3. What is the main benefit of binding a configuration section to a strongly-typed class (e.g. ApiSettings) instead of reading each value individually with the indexer?

Show answer

Correct: B

Why B is correct: Binding converts the raw string values into your class's actual property types (int, bool, etc.) once, in one place, and gives you IntelliSense and compiler checking everywhere you use apiSettings.RetryCount, instead of repeating and mistyping raw key strings throughout your codebase.

Why A is incorrect: Binding happens after the file is already loaded into memory; it doesn't affect load speed.

Why C is incorrect: You still need a configuration source (like the JSON file) to bind from — binding is a read-time convenience, not a replacement for having settings somewhere.

Why D is incorrect: Binding is purely about mapping values to types; it has nothing to do with encryption.

Reinforcement: Strongly-typed binding trades scattered string keys for a single, safe, reusable settings object.

4. Your appsettings.json defines a full "Smtp" object with Host, Port, and FromAddress. In production you only set the environment variable Smtp__Host. What happens to Port and FromAddress?

Show answer

Correct: B

Why B is correct: Overriding happens per individual key, not per object/section. Setting Smtp__Host only replaces the value for that one key; Smtp:Port and Smtp:FromAddress are untouched and still come from appsettings.json.

Why A is incorrect: This is the common misconception this question is testing — overriding is granular, not all-or-nothing at the section level.

Why C is incorrect: The section isn't "incomplete" — the other two keys are still fully present from the base file.

Why D is incorrect: No command-line arguments were set in this scenario, so they play no role here.

Reinforcement: Think key-by-key, not file-by-file, when reasoning about overrides.

You now understand how .NET layers configuration from multiple sources and how to bind it into strongly-typed settings classes.


dotnetmadeeasy.com — Learn C# and .NET, the right way.