Configuration and logging
Read settings from files, environment variables and secrets with IConfiguration and the options pattern — and log with ILogger.
12 min read
You will learn
- Explain how .NET layers configuration sources
- Bind settings to classes with the options pattern
- Keep secrets out of source code with user secrets
- Write structured logs with ILogger and log levels
Configuration in .NET
Every .NET app built on the generic host (ASP.NET Core, worker services, console apps using Host.CreateApplicationBuilder) gets a ready-made configuration system that merges several sources into one view, IConfiguration:
appsettings.json (lowest priority)
appsettings.{Environment}.json e.g. appsettings.Development.json
user secrets (Development only)
environment variables
command-line arguments (highest priority)Later sources override earlier ones, so you commit sensible defaults and override per environment without touching code.
// appsettings.json
{
"Smtp": {
"Host": "smtp.example.com",
"Port": 25,
"From": "shop@example.com"
},
"Features": { "NewCheckout": false }
}var builder = WebApplication.CreateBuilder(args);
string host = builder.Configuration["Smtp:Host"]!; // ':' navigates sections
int port = builder.Configuration.GetValue<int>("Smtp:Port");Environment variables use double underscores for nesting: Smtp__Port=587. The environment name itself comes from ASPNETCORE_ENVIRONMENT / DOTNET_ENVIRONMENT (Development, Staging, Production).
The options pattern
Reading strings by key all over your code is fragile. Bind a section to a class instead:
public class SmtpOptions
{
public const string Section = "Smtp";
[Required] public string Host { get; set; } = "";
[Range(1, 65535)] public int Port { get; set; } = 25;
public string From { get; set; } = "";
}
builder.Services.AddOptions<SmtpOptions>()
.BindConfiguration(SmtpOptions.Section)
.ValidateDataAnnotations()
.ValidateOnStart(); // fail fast at startup if config is invalidThen inject it wherever it's needed:
public class EmailSender(IOptions<SmtpOptions> options)
{
private readonly SmtpOptions smtp = options.Value;
}IOptionsSnapshot<T> re-reads per request and IOptionsMonitor<T> notifies on changes, for settings that can change while the app runs.
Secrets
Never commit passwords or API keys. In development, use user secrets — stored in your user profile, outside the repository, and merged into configuration automatically:
dotnet user-secrets init
dotnet user-secrets set "Smtp:Password" "s3cr3t"
dotnet user-secrets listIn production, use environment variables injected by the platform or a vault (Azure Key Vault has a configuration provider).
Logging with ILogger
.NET's logging abstraction, ILogger<T>, is injected like any service and writes to any number of providers — console, debug output, files, Seq, Application Insights, OpenTelemetry:
public class OrderService(ILogger<OrderService> logger)
{
public void Place(Order order)
{
logger.LogInformation("Placing order {OrderId} for {CustomerId}", order.Id, order.CustomerId);
try
{
...
}
catch (PaymentException ex)
{
logger.LogError(ex, "Payment failed for order {OrderId}", order.Id);
throw;
}
}
}Levels
| Level | Use for |
|---|---|
Trace / Debug | Detailed diagnostics — usually off in production |
Information | Normal significant events: request handled, order placed |
Warning | Unexpected but handled: retrying, falling back to defaults |
Error | An operation failed |
Critical | The app can't continue: out of disk, can't reach the database at all |
Minimum levels are set per category in configuration:
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Shop.Payments": "Debug"
}
}Structured logging
Use message templates, not string interpolation. {OrderId} becomes a named property that log tools can filter and aggregate — and the string is only built if that level is enabled. Popular libraries like Serilog plug into the same ILogger API.
High-performance logging
In hot paths, the [LoggerMessage] source generator produces logging methods with zero allocations: [LoggerMessage(Level = LogLevel.Information, Message = "Order {OrderId} placed")] static partial void OrderPlaced(ILogger logger, int orderId);
Key takeaways
- Configuration merges JSON files, user secrets, environment variables and command-line args — later wins.
- The options pattern binds sections to typed, validated classes injected as
IOptions<T>. - Keep secrets in user secrets (dev) and vaults or environment variables (prod) — never in git.
- Log with
ILogger<T>, choose levels deliberately, and use message templates for structured logs.
Quiz
4 questions · 3 correct to pass1.appsettings.json sets "Smtp:Port": 25 and the environment variable Smtp__Port is 587. Which value wins?
2.What does the options pattern give you?
3.Which log level should "Order 42 placed" use in a typical app?
4.Why write `logger.LogInformation("Order {OrderId} placed", id)` instead of `logger.LogInformation($"Order {id} placed")`?
To complete this lesson
- ○ Pass the quiz (3/4 correct)