.NET Core Configuration in .NET 8
In this post, I will explore the evolution of configuration in .NET, how the framework handles configuration, and provide practical examples to help you implement these concepts in your applications.
What is Configuration?
Configuration in .NET refers to settings that control the behavior of applications. These settings can include values like retry times, queue lengths, feature flags, and secrets such as connection strings. Traditionally, configuration was applied at compile time, but modern .NET allows for dynamic runtime configuration, enhancing flexibility and power.
Evolution of .NET Configuration
Historically, .NET Framework (versions 4, 4.7, 4.8) used XML-based WebConfig files, accessed via a configuration manager. While this method allowed key-value pair configurations, it lacked support for dependency injection and had a cumbersome transformation syntax.
Here’s an example of a WebConfig file used in .NET Framework:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<appSettings>
<add key="RetryCount" value="5" />
<add key="QueueLength" value="100" />
</appSettings>
<connectionStrings>
<add name="MyDatabase" connectionString="Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;" providerName="System.Data.SqlClient" />
</connectionStrings>
</configuration>
With the advent of .NET Core 5, 6, 7, and now 8, the configuration system was revamped to support various sources like JSON, XML, environment variables, command-line arguments, and even custom providers. These sources are processed in a defined order, providing a unified and dynamic configuration system.
Configuration in appsettings.json
In .NET Core and later versions, JSON is commonly used for configuration. Here is an example of an appsettings.json
file:
{
"AppSettings": {
"RetryCount": 5,
"QueueLength": 100,
"Greeting": "Hello, World!",
"Environment": "Development"
},
"ConnectionStrings": {
"MyDatabase": "Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Configuration in Environment Variables
Set the following environment variables
- Name:
AppSettings__Environment
- Value:
Production
Configuration in Command Line Arguments
To add command line arguments in a .NET application, you can pass them when running your application. Here’s an example:
dotnet run -- AppSettings:Environment=Staging Logging:LogLevel:Default=Debug
In the above example, AppSettings:Environment
and Logging:LogLevel:Default
are the command line arguments being passed.
Overriding Configuration Values
The order in which configuration sources are added determines which values override others. The last provider added will have the highest precedence.
For example, let’s assume that you are adding the configurations the following order
- JSON file (
appsettings.json
) - Environment variables
- Command line arguments
Here’s how you can set up the configuration in a .NET Core application with appsettings.json, Environmental Variables and Command Line arguments.
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.AddCommandLine(args);
IConfiguration configuration = builder.Build();
In this setup:
- Values in
appsettings.json
are loaded first. - Environment variables can override values from
appsettings.json
. - Command line arguments can override both
appsettings.json
and environment variables.
Example Scenarios
Say in a .NET solution you have the following configurations. You have an appsettings.json file with the following configuration and then you set the environment variables and then run the application with the command shown in step 2
Step 1 — appsetting.json
{
"AppSettings": {
"Environment": "Development",
"RetryCount": 5
}
}
Step 2 — Environmental Variable
- Name:
AppSettings__Environment
- Value:
Production
Step 3 — Run the application
dotnet run --AppSettings:Environment=Staging --AppSettings:RetryCount=10
When the application runs, the final values will be:
AppSettings:Environment
will beStaging
(overridesProduction
from environment variables andDevelopment
fromappsettings.json
).AppSettings:RetryCount
will be10
(overrides5
fromappsettings.json
).
Using User Secrets for Development
User secrets are a valuable tool for managing sensitive information during development. They enable secure storage and access to secrets without checking them into source control.
User secrets can be set on your local machine with the following command. Refer this link for setting up user secrets in a Windows environment.
Imagine a scenario where User Secrets, the appsettings.json file, Environmental Variables, and Command Line arguments are all used. The sequence of evaluation or reading the configuration would typically be as follows:
- appsettings.json
- User Secrets
- Environmental Variables
- Command Line Arguments
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Configure the order of configuration sources
builder.Configuration
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables()
.AddCommandLine(args);
if (builder.Environment.IsDevelopment())
{
builder.Configuration.AddUserSecrets<Program>();
}
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Set the user secret using the following command after you’ve setup the user secrets mechanism in your local development machine.
dotnet user-secrets set "AppSettings:Environment" "Development"
Accessing secrets from a cloud provider’s Vault is a topic extensive enough to merit its own blog post, which I will save for a future discussion.
IOptions Pattern
The IOptions pattern in .NET provides a robust way to manage configuration settings with support for reloading and named options. There are three main choices: IOptions
, IOptionsSnapshot
, and IOptionsMonitor
. Each serves different purposes and scenarios, particularly in applications requiring dynamic updates or multiple configurations.
public class AppSettings
{
public string Greeting { get; set; }
public string Environment { get; set; }
}
public class MyService
{
private readonly AppSettings _settings;
public MyService(IOptions<AppSettings> options)
{
_settings = options.Value;
}
public void ShowSettings()
{
Console.WriteLine($"Greeting: {_settings.Greeting}, Environment: {_settings.Environment}");
}
}
.NET 8 introduces compile-time validation for configurations, enhancing efficiency. It supports data annotations and custom validation logic, ensuring configuration integrity before application startup.
public class AppSettings
{
[Required]
public string Greeting { get; set; }
[Required]
[Url]
public string Environment { get; set; }
}
var appSettings = new AppSettings();
configuration.GetSection("AppSettings").Bind(appSettings);
Validator.ValidateObject(appSettings, new ValidationContext(appSettings), validateAllProperties: true);
There are much better or rather cleaner ways to validate configuration values at startup which too I will reserve it for another blog post sometime later.