BLOG · .NET · BEST PRACTICES

The Options Pattern in .NET: typed configuration in 3 lines of code

Illustration: a code window with braces and colored lines, symbolizing typed .NET configuration with IOptions

Still using appsettings.json with Configuration["api_url"] scattered across your C# classes? And discovering config errors only when production breaks?

With the Options Pattern, your configuration becomes a typed C# object, validated at application startup. I use it in every project.

The problem: magic strings

Without the Options Pattern, your code looks like this:

var apiUrl = _configuration["ApiSettings:BaseUrl"];
var timeout = _configuration.GetValue<int>("ApiSettings:TimeoutInSeconds");

The result: magic strings everywhere, silent typos, and if the config structure changes, you have to touch every class that uses it.

The solution, in 4 steps

1, The section in appsettings.json

{
  "ApiConfig": {
    "Token": "a1b2c3d4e5f6g7h8i9j0",
    "ApiUrl": "https://api.antoine.fr",
    "TimeoutInSeconds": 30
  }
}

2, The C# class that represents it

public class ApiConfig
{
    public string Token { get; init; }
    public string ApiUrl { get; init; }
    public int TimeoutInSeconds { get; init; }
}

3, The registration in Program.cs: the famous 3 lines

builder.Services
    .AddOptions<ApiConfig>()
    .BindConfiguration("ApiConfig")
    .ValidateDataAnnotations();

4, Typed injection into your services

public class MyService
{
    private readonly HttpClient _httpClient;
    private readonly ApiConfig _apiConfig;

    public MyService(HttpClient httpClient, IOptions<ApiConfig> options)
    {
        _httpClient = httpClient;
        _apiConfig = options.Value;
    }

    public async Task<string> GetDataFromApiAsync()
    {
        var response = await _httpClient.GetAsync($"{_apiConfig.ApiUrl}/endpoint");
        response.EnsureSuccessStatusCode();
        return await response.Content.ReadAsStringAsync();
    }
}

What it changes day to day

Organized: one dedicated class per section of your appsettings.json.

Typed: you access options.ApiUrl instead of Configuration["api_url"], a property instead of a magic string.

Safe: impossible to misspell the config, your IDE warns you if the property doesn't exist.

Validated: add [Required] on your properties to check their presence at startup, no more bad surprises in production.

Simple: 3 lines in Program.cs and it's set up for the whole app.

Adding a new config?

Need a Datadog object in your appsettings.json? Create a DatadogSettings class mirroring the section, register it in Program.cs, and it's ready to use, typed and validated.

Takeaway: the Options Pattern takes minutes to set up and durably improves the readability, maintainability and robustness of your .NET projects.

This article comes from one of my LinkedIn posts (in French), join the discussion ↗