.NET Configuration in Depth

Chris Ayers

Chris Ayers

Senior Customer Engineer
Microsoft

Twitter : @Chris_L_Ayers
Mastodon: @Chrisayers@hachyderm.io
LinkedIn: chris-l-ayers
Blog: https://chris-ayers.com/
GitHub: Codebytes

Agenda

  • What is configuration?
  • How does .NET Framework handle configuration?
  • How does .NET and ASP.NET handle configuration?
  • Configuration Providers
  • Configuration Binding
  • The Options Pattern
  • Questions?

What is Configuration?

Settings

  • Retry Times
  • Queue Length

Feature Flags

  • Per User
  • Percentage

Secrets

  • Connection Strings
  • App Registration

When is configuration applied?

Compile Time

Run Time

.NET Framework Configuration

Web.Config

  • Limited to Key-Value string pairs
  • Accessed through a static ConfigurationManager Class
  • Dependency Injection was not provided out of the box
  • Transformation through difficult syntax
    • Slow Cheetah
  • Hard to unit test
  • Easy to leak secrets

XML, Static Classes, and Parsing

  <appSettings>
    <add key="webpages:Version" value="3.0.0.0" />
    <add key="webpages:Enabled" value="false" />
    <add key="ClientValidationEnabled" value="true" />
    <add key="UnobtrusiveJavaScriptEnabled" value="true" />
    <add key="Greeting" value="Hello, Everyone!" />
    <add key="CurrentMajorDotNetVersion" value="6" />
  </appSettings>
  <system.web>
    <compilation debug="true" targetFramework="4.7.2" />
    <httpRuntime targetFramework="4.7.2" />
  </system.web>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <connectionStrings>
    <add name="MyDB"
      connectionString="Data Source=ReleaseSQLServer..."
      xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/>
  </connectionStrings>

  private string greeting = "";
  private int majorDotNetVersion = 0;
  public HomeController()
  {
      greeting = ConfigurationManager.AppSettings["Greeting"];
      string ver = ConfigurationManager.AppSettings["CurrentMajorDotNetVersion"]
      majorDotNetVersion = Int32.Parse(ver);
  }

.NET Core/5/6/7/8
ASP.NET Configuration

Configuration Providers

center

Order Matters

center

Binding

{
    "Settings": {
        "KeyOne": 1,
        "KeyTwo": true,
        "KeyThree": {
            "Message": "Oh, that's nice...",
            "SupportedVersions": {
                "v1": "1.0.0",
                "v3": "3.0.7"
            }
        }
    }
}
public sealed class Settings
{
    public required int KeyOne { get; set; }
    public required bool KeyTwo { get; set; }
    public required NestedSettings KeyThree { get; set; };
}
public sealed class NestedSettings
{
    public required string Message { get; set; };
}

IConfigurationRoot config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .Build();
    
Settings? settings = 
  config.GetRequiredSection("Settings")
    .Get<Settings>();

Hierarchical Configuration Data

Keys are Flattened

{
  "Parent": {
    "FavoriteNumber": 7,
    "Child": {
      "Name": "Example",
      "GrandChild": {
        "Age": 3
      }
    }
  }
}
{
  "Parent:FavoriteNumber": 7,
  "Parent:Child:Name": "Example",
  "Parent:Child:GrandChild:Age": 3
}

Out of the Box

Console

  • No Configuration

.NET Generic Host

  • JSON
    • appsettings.json
    • appsettings.{Environment}.json
  • User Secrets
  • Environment Variables
  • Command Line Variables

ASP.NET

  • JSON
    • appsettings.json
    • appsettings.{Environment}.json
  • User Secrets
  • Environment Variables
  • Command Line Variables

Configuration Providers

File-based

  • JSON
  • XML
  • INI
  • Key-per-file

Others

  • Environment variables
  • Command-line
  • In-Memory
  • User Secrets
  • Azure Key Vault
  • Azure App Configuration

Json Provider

<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" />
IHostEnvironment env = builder.Environment;

builder.Configuration
  .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
  .AddJsonFile($"appsettings.{env.EnvironmentName}.json", true, true);
{
    "SecretKey": "Secret key value",
    "TransientFaultHandlingOptions": {
        "Enabled": true,
        "AutoRetryDelay": "00:00:07"
    },
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        }
    }
}

XML Provider

<PackageReference Include="Microsoft.Extensions.Configuration.Xml" Version="8.0.0" />
IHostEnvironment env = builder.Environment;

builder.Configuration
  .AddXmlFile("appsettings.xml", optional: true, reloadOnChange: true)
  .AddXmlFile("repeating-example.xml", optional: true, reloadOnChange: true);
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <SecretKey>Secret key value</SecretKey>
  <TransientFaultHandlingOptions>
    <Enabled>true</Enabled>
    <AutoRetryDelay>00:00:07</AutoRetryDelay>
  </TransientFaultHandlingOptions>
  <Logging>
    <LogLevel>
      <Default>Information</Default>
      <Microsoft>Warning</Microsoft>
    </LogLevel>
  </Logging>
</configuration>

INI Provider

<PackageReference Include="Microsoft.Extensions.Configuration.Ini" Version="8.0.0" />
IHostEnvironment env = builder.Environment;

builder.Configuration
  .AddIniFile("appsettings.ini", optional: true, reloadOnChange: true)
  .AddIniFile($"appsettings.{env.EnvironmentName}.ini", true, true);
SecretKey="Secret key value"

[TransientFaultHandlingOptions]
Enabled=True
AutoRetryDelay="00:00:07"

[Logging:LogLevel]
Default=Information
Microsoft=Warning

Environment Variables

  • Typically used to override settings found in appsettings.json or user secrets
  • the : delimieter doesn't work for Hierarchical data on all platforms
  • the __ delimieter is used instead of :
public class TransientFaultHandlingOptions
{
    public bool Enabled { get; set; }
    public TimeSpan AutoRetryDelay { get; set; }
}
set SecretKey="Secret key from environment"
set TransientFaultHandlingOptions__Enabled="true"
set TransientFaultHandlingOptions__AutoRetryDelay="00:00:13"

Environment Variables

  • There are built-in prefixes, like
    • ASPNETCORE_ for ASP.NET Core
    • DOTNET_ for .NET Core
  • You can provide your own prefix
builder.Configuration.AddEnvironmentVariables(
  prefix: "MyCustomPrefix_");
set MyCustomPrefix_MyKey="My key with MyCustomPrefix_"
set MyCustomPrefix_Position__Title=Editor_with_custom
set MyCustomPrefix_Position__Name=Environment
dotnet run

DEMOS

The Options Pattern

Interface Segregation Principle (ISP): Scenarios (classes) that depend on configuration settings depend only on the configuration settings that they use.

Separation of Concerns : Settings for different parts of the app aren't dependent or coupled to one another.

An Options Class

  • Must be non-abstract with a public parameterless constructor
  • Contain public read-write properties to bind (fields are not bound)
public class FileOptions
{
    public string FileExtension { get; set; } ="";
    public string OutputDir { get; set; } ="";
    public string TemplateFile { get; set; } ="";
}

Types of IOptions

Singleton Reloading Support Named Option Support
IOptions
Yes No No
IOptionsSnapshot
No Yes Yes
IOptionsMonitor
Yes Yes Yes

DEMOS

Questions?

Contact

Twitter: @Chris_L_Ayers
Mastodon: @Chrisayers@hachyderm.io
LinkedIn: - chris-l-ayers
Blog: https://chris-ayers.com/
GitHub: Codebytes