Configuration and the Options Pattern: appsettings Layers and Separating Settings from Code
We cover separating settings from code: appsettings files and environment layers, managing connection strings safely, reading settings with strong types via the Options pattern, and keeping secrets out of code.
So far we've embedded many values directly in the code: the database connection string, the log file path. It worked, but it's a bad habit. Because these values change by environment; you use one database on your development machine and a completely different one on the live server. You don't want to edit the code and recompile with every change. Today we talk about how to separate settings from code and how elegantly .NET solves this.
Why Do We Separate Settings from Code?
Let's start with a simple principle: things that change and the logic should stay separate from each other. The code itself describes what will be done and rarely changes. Settings say where and with which values things will run, and change often. When you mix these two, you have to touch the code even for a small value change.
Think of it like a device's user manual and its control knobs. The manual (the code) describes how the device works and is fixed. The knobs (the settings) are turned according to the user's need. No one redraws the device's circuit diagram to change the volume; they just turn the knob. A good application should be like this too: to change its behavior, you edit the settings, not the code.
appsettings.json: The Home of Settings
In .NET projects, the natural home of settings is the appsettings.json file. We saw this file get created while setting up the project in the second article. You put the settings the application needs into it in an organized structure. For example, let's move the database connection string and the log setting from the previous article here:
{
"ConnectionStrings": {
"Default": "Data Source=app.db"
},
"Logging": {
"FilePath": "logs/app.txt"
}
}
Now these values aren't embedded in the code, but outside, in an editable file. When you need to change the connection string, there's no need to compile the code; you just update this file. So how do we read these values from within the code?
The Simple Way to Read Settings
.NET reads this file automatically and presents its contents to you through a structure called IConfiguration. Inside Program.cs, you can pull the connection string like this:
var connectionString = builder.Configuration
.GetConnectionString("Default");
builder.Services.AddDbContext(options =>
options.UseSqlite(connectionString));
In the ninth article we wrote this connection string directly in the code; now we read it from the file. GetConnectionString("Default") brings the Default value under the ConnectionStrings section in appsettings.json. The code no longer knows which database to connect to; it now asks the settings file for that.
Environment Layers: Development and Production
Here's one of .NET's most elegant features. The settings you use while developing and the settings in production are different. .NET solves this with layered settings files. Alongside the main appsettings.json, you can keep environment-specific files:
appsettings.json // Base settings for all environments
appsettings.Development.json // Development environment only
appsettings.Production.json // Live environment only
The logic works like this: first the base file is read, then the current environment's file is added on top, and conflicting values are replaced with the environment-specific one. So while working in the development environment, the base settings are valid, but the values in appsettings.Development.json override them. This way, you can make distinctions like "local database in development, real database in production" without writing a single line of code. The application understands which environment it's running in from an environment variable and selects the right file itself.
The Options Pattern: Reading Settings with Strong Types
Pulling values from IConfiguration one by one, by name, is fine for small needs. But when you have multiple related settings, reading them with string keys every time is both error-prone and messy. This is where the Options pattern comes in: you bind related settings to a C# class and access them with safe, strong types.
Let's say we have a few settings for logging. First we write a plain class representing them:
public class LoggingOptions
{
public string FilePath { get; set; }
public int RetentionDays { get; set; }
}
Then we bind the relevant section in appsettings.json to this class:
builder.Services.Configure(
builder.Configuration.GetSection("Logging"));
This line takes the Logging section in appsettings.json and places it into the fields of the LoggingOptions class; just like the model binding in the seventh article, it fills automatically as the names match. Now, inside a service, you can reach these settings directly, in a type-safe way:
public class LogArchiver
{
private readonly LoggingOptions _options;
public LogArchiver(IOptions options)
{
_options = options.Value;
}
}
We ask for IOptions<LoggingOptions> in the constructor, and reach the actual settings object with .Value. Now when you write _options.FilePath, your editor gives you autocomplete and the risk of writing a wrong key name disappears. Instead of wrestling with string keys, the settings are now as safe as a part of the code.
Where Should Sensitive Data Go?
A critical warning: sensitive data like passwords, API keys, and real database passwords shouldn't be written in appsettings.json. Because this file is usually included in the code repository (like Git), and a secret you put there ends up shared with the whole team and sometimes the whole world. In the development environment, .NET has the User Secrets feature for this; it keeps secrets outside the project, in a place specific to your machine. In the live environment, environment variables or a dedicated secret management service is used. The rule is simple: secrets should never enter the code repository.
A Small Experiment
Move the database connection string you embedded in the code in the ninth article to appsettings.json and update Program.cs to read this value from the file. Then create an appsettings.Development.json file, put a different connection string in it, run the application in the development environment, and observe which one is valid. If you like, take one more step and bind a small settings group to a class with the Options pattern. This experiment personally shows how relieving it is to separate settings from code.
In the next article we'll get into async/await: we'll talk about what the async and await keywords you frequently see in the code examples in this series actually do, why they matter so much in an API, and the effect of correct asynchronous usage on performance. We've used them all along; now it's time to understand how they work.