.NET Academy

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 invalid

Then 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 list

In 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

LevelUse for
Trace / DebugDetailed diagnostics — usually off in production
InformationNormal significant events: request handled, order placed
WarningUnexpected but handled: retrying, falling back to defaults
ErrorAn operation failed
CriticalThe 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 pass
  1. 1.appsettings.json sets "Smtp:Port": 25 and the environment variable Smtp__Port is 587. Which value wins?

  2. 2.What does the options pattern give you?

  3. 3.Which log level should "Order 42 placed" use in a typical app?

  4. 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)