How Dynamic Configuration Works
How Dynamic Configuration Works
Diginsight provides a powerful dynamic configuration system that allows configuration values to be loaded from multiple sources and overridden at runtime.
This article explains how Diginsight manages configuration loading from files, HTTP headers, and volatile settings, and how you can scope configurations to specific classes using class-aware notation.
Configuration Sources Overview
Diginsight supports three main configuration sources:
| Source | Scope | Persistence | Use Case |
|---|---|---|---|
| File Configuration | Application-wide | Persistent | Default values, environment-specific settings |
| Dynamic Configuration | Request scope | Per-request | Per-request overrides via HTTP headers |
| Volatile Configuration | Application-wide | Runtime (until restart) | Hot-switch settings without redeploy |
┌─────────────────────────────────────────────────────────────────┐
│ Configuration Priority │
│ (later sources override earlier ones) │
│ │
│ 1. appsettings.json ──────────────────────────────► │
│ 2. appsettings.{Environment}.json ─────────────────► │
│ 3. Volatile Configuration (runtime storage) ───────► │
│ 4. Dynamic Configuration (HTTP headers) ───────────► FINAL │
│ │
└─────────────────────────────────────────────────────────────────┘
1. File-Based Configuration
File-based configuration is the foundation. Options are loaded from appsettings.json using the standard .NET configuration system:
{
"Diginsight": {
"Activities": {
"LogBehavior": "Show",
"ActivityLogLevel": "Debug",
"LoggedActivityNames": {
"SmartCache.SetValue": "Hide",
"SmartCache.OnEvicted": "Hide",
"ServiceBusReceiver.Complete": "Hide"
}
}
}
}Register in Program.cs or Startup.cs:
services.ConfigureClassAware<DiginsightActivitiesOptions>(
configuration.GetSection("Diginsight:Activities")
);2. Dynamic Configuration (HTTP Headers)
Dynamic configuration allows per-request overrides via the Dynamic-Configuration HTTP header.
Values last only for the duration of a single request/scope.
How It Works
- HTTP Request arrives with
Dynamic-Configurationheader DefaultDynamicConfigurationLoaderextracts key-value pairs from the headerDynamicallyConfigureOptionsbuilds an in-memoryIConfigurationfrom these pairs- Configuration is bound to the options object via the Filler pattern
Header Format
GET /api/weather HTTP/1.1
Dynamic-Configuration: LogBehavior=Show MaxAge=0 DisablePayloadRendering=true
Multiple values are space-separated: Key1=Value1 Key2=Value2 Key3=Value3
Registering Dynamic Configuration
To enable dynamic configuration for an options class:
services.ConfigureClassAware<DiginsightActivitiesOptions>(
configuration.GetSection("Diginsight:Activities"))
.DynamicallyConfigureClassAware<DiginsightActivitiesOptions>();Configuration Flow
HTTP Header: "Dynamic-Configuration: LogBehavior=Show MaxAge=0"
│
▼
┌──────────────────────────────────────────────────────────┐
│ DefaultDynamicConfigurationLoader.Load() │
│ 1. Get header value from HttpContext │
│ 2. Parse w. DynamicHttpHeadersParser.ParseConfiguration │
│ 3. Return KeyValuePair<string, string?>[] │
└──────────────────────┬───────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────┐
│ DynamicallyConfigureOptions.ConfigureCore() │
│ 1. Build IConfiguration from key-value pairs │
│ 2. Wrap with FilteredConfiguration (for class-aware) │
│ 3. configuration.Bind(options.MakeFiller()) │
└──────────────────────────────────────────────────────────┘
3. Volatile Configuration
Volatile configuration provides runtime-persistent overrides that survive across requests but are lost on application restart. Unlike dynamic configuration (per-request), volatile settings are stored in memory and apply to all requests.
Use Cases
- Hot-switching feature flags without redeployment
- Temporarily changing log levels for debugging
- A/B testing different configurations
How It Works
Volatile configuration uses an IVolatileConfigurationStorage that holds configuration values in memory. When options are resolved, the volatile configuration is checked and applied.
Registering Volatile Configuration
services.ConfigureClassAware<MyOptions>(configuration.GetSection("MyOptions"))
.VolatilelyConfigureClassAware<MyOptions>();4. Class-Aware Configuration
Class-aware configuration allows you to scope settings to specific classes, enabling component-level or class-level configuration overrides.
The Class Marker Notation
In configuration files, use the @ symbol followed by the class name to scope settings:
{
"Diginsight": {
"Activities": {
"LogBehavior": "Hide",
"LogBehavior@MyNamespace.ImportantService": "Show",
"LogBehavior@MyNamespace.VerboseService": "Truncate"
}
}
}This means: - Default LogBehavior is Hide - For class MyNamespace.ImportantService, LogBehavior is Show - For class MyNamespace.VerboseService, LogBehavior is Truncate
HTTP Header Class-Aware Syntax
The same notation works in HTTP headers:
Dynamic-Configuration: LogBehavior=Hide LogBehavior@MyService=Show
How FilteredConfiguration Works
The FilteredConfiguration class wraps an IConfiguration and filters sections based on the class context:
public class FilteredConfiguration : IFilteredConfiguration
{
public const char ClassDelimiter = '@';
// When accessing "LogBehavior", FilteredConfiguration:
// 1. Looks for "LogBehavior@FullClassName" (exact match)
// 2. Falls back to "LogBehavior@ClassName" (short name)
// 3. Falls back to "LogBehavior" (no class marker)
}The class marker matching uses a priority system - more specific markers take precedence.
Using Class-Aware Options
To consume class-aware options, inject IClassAwareOptionsMonitor<T>:
public class MyService
{
private readonly IClassAwareOptionsMonitor<DiginsightActivitiesOptions> optionsMonitor;
public MyService(IClassAwareOptionsMonitor<DiginsightActivitiesOptions> optionsMonitor)
{
this.optionsMonitor = optionsMonitor;
}
public void DoWork()
{
// Get options scoped to THIS class
var options = optionsMonitor.Get(GetType());
// LogBehavior will be resolved based on class-aware markers
if (options.LogBehavior == LogBehavior.Show)
{
// ...
}
}
}5. The Filler Pattern: Partial Dynamic Configuration
The Filler pattern is a powerful mechanism that allows you to control which properties of an options class can be dynamically configured, and how complex types are serialized/deserialized for configuration binding.
Why Use a Filler?
When configuration is bound from HTTP headers or volatile storage, .NET’s IConfiguration.Bind() is used. However:
- Not all properties should be dynamically configurable - some are sensitive or should only be set at startup
- Complex types need conversion - dictionaries, collections, and custom types need string serialization/deserialization
- Different property names - the configuration key name might differ from the property name
The IDynamicallyConfigurable Interface
public interface IDynamicallyConfigurable
{
/// <summary>
/// Returns an object that "masks" the properties available for dynamic configuration.
/// </summary>
object MakeFiller();
}The MakeFiller() method returns either: - this (default): All public properties are dynamically configurable - A custom Filler class: Only properties defined on the Filler are configurable
Creating a Custom Filler
Here’s an example showing the Filler pattern in action:
public class MyOptions : IDynamicallyConfigurable
{
// These properties CAN be configured from files
public string? Foo { get; set; }
public double Baz { get; set; }
public ICollection<string> Bars { get; private set; } = new List<string>();
// This property should NOT be dynamically configurable
public string? SensitiveValue { get; set; }
// Return the Filler to control dynamic configuration
object IDynamicallyConfigurable.MakeFiller() => new Filler(this);
private class Filler
{
private readonly MyOptions filled;
public Filler(MyOptions filled) => this.filled = filled;
// Foo is exposed unchanged
public string? Foo
{
get => filled.Foo;
set => filled.Foo = value;
}
// Bars collection is exposed as a semicolon-separated string
public string Bar
{
get => string.Join(";", filled.Bars);
set => filled.Bars = value.Split(';').ToList();
}
// Baz is NOT exposed - cannot be dynamically configured
// SensitiveValue is NOT exposed - cannot be dynamically configured
}
}What the Filler Controls
| Aspect | Description |
|---|---|
| Property Visibility | Only properties defined on Filler can be dynamically configured |
| Property Name | Filler property name is the configuration key name |
| Type Conversion | Filler handles serialization (getter) and deserialization (setter) |
| Validation | Filler setters can validate and sanitize input |
Real-World Example: DiginsightActivitiesOptions
The DiginsightActivitiesOptions class demonstrates the Filler pattern for dictionary properties:
public sealed class DiginsightActivitiesOptions : IDynamicallyConfigurable, IVolatilelyConfigurable
{
// Dictionary property - configured from JSON as nested object
public IDictionary<string, LogBehavior> LoggedActivityNames { get; }
// Simple properties
public LogBehavior LogBehavior { get; set; }
public LogLevel ActivityLogLevel { get; set; }
public bool DisablePayloadRendering { get; set; }
object IDynamicallyConfigurable.MakeFiller() => new Filler(this);
object IVolatilelyConfigurable.MakeFiller() => new Filler(this);
private class Filler
{
private readonly DiginsightActivitiesOptions filled;
public Filler(DiginsightActivitiesOptions filled) => this.filled = filled;
// Simple properties pass through unchanged
public LogBehavior LogBehavior
{
get => filled.LogBehavior;
set => filled.LogBehavior = value;
}
public LogLevel ActivityLogLevel
{
get => filled.ActivityLogLevel;
set => filled.ActivityLogLevel = value;
}
public bool DisablePayloadRendering
{
get => filled.DisablePayloadRendering;
set => filled.DisablePayloadRendering = value;
}
// Dictionary is exposed as space-separated key=value pairs
public string LoggedActivityNames
{
get => string.Join(" ", filled.LoggedActivityNames.Select(kv => $"{kv.Key}={kv.Value}"));
set
{
// Skip if unchanged (prevents unnecessary clear/repopulate)
if (value == string.Join(" ", filled.LoggedActivityNames.Select(kv => $"{kv.Key}={kv.Value}")))
return;
filled.LoggedActivityNames.Clear();
filled.LoggedActivityNames.AddRange(
value.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.Select(x => x.Split('=', 2) switch
{
[var k] => KeyValuePair.Create(k, LogBehavior.Show),
[var k, var v] when Enum.TryParse(v, true, out LogBehavior b) => KeyValuePair.Create(k, b),
_ => (KeyValuePair<string, LogBehavior>?)null,
})
.OfType<KeyValuePair<string, LogBehavior>>()
);
}
}
}
}HTTP Header Format for Dictionary Properties
With the Filler above, you can override LoggedActivityNames via HTTP header:
Dynamic-Configuration: LoggedActivityNames=SmartCache.SetValue=Hide%20SmartCache.OnEvicted=Show
Note: Space is URL-encoded as %20 since space separates top-level entries.
6. Configuration Binding Flow
The complete flow from configuration source to options instance:
┌─────────────────────────────────────────────────────────────────┐
│ Options Resolution Flow │
└─────────────────────────────────────────────────────────────────┘
1. ClassAwareOptionsFactory.Create(name, @class) is called
│
▼
2. Create TOptions instance via Activator.CreateInstance<TOptions>()
│
▼
3. Run IConfigureOptions<TOptions> configurators
└── Binds appsettings.json via ConfigurationBinder
│
▼
4. Run IConfigureClassAwareOptions<TOptions> configurators
└── Including DynamicallyConfigureClassAwareOptions:
a. Load specs from HTTP header via IDynamicConfigurationLoader
b. Build in-memory IConfiguration from specs
c. Wrap with FilteredConfiguration.For(configuration, @class)
d. configuration.Bind(options.MakeFiller())
│
▼
5. Run IPostConfigureOptions<TOptions> post-configurators
│
▼
6. Run IPostConfigureClassAwareOptions<TOptions> post-configurators
└── Including VolatilelyConfigureClassAwareOptions:
a. Get configuration from IVolatileConfigurationStorage
b. Wrap with FilteredConfiguration.For(configuration, @class)
c. configuration.Bind(options.MakeFiller())
│
▼
7. Run validators
│
▼
8. Return configured TOptions instance
7. Best Practices
Filler Design Guidelines
- Only expose safe properties - Don’t expose sensitive configuration to dynamic override
- Use proper serialization - Always explicitly format complex types (don’t rely on
ToString()) - Add setter guards - Check if value is unchanged before clearing/repopulating collections
- Validate input - Setters can reject invalid values
Configuration Naming
- Use consistent naming - Filler property names become configuration keys
- Document expected format - For complex types, document the string format expected
Class-Aware Configuration
- Use full class names -
@MyNamespace.MyClassis more specific than@MyClass - Order by specificity - More specific markers override less specific ones
- Test class resolution - Verify the correct configuration is resolved for each class
8. Summary
| Concept | Purpose |
|---|---|
| File Configuration | Base configuration from appsettings.json |
| Dynamic Configuration | Per-request overrides via HTTP headers |
| Volatile Configuration | Runtime-persistent overrides |
| Class-Aware | Scope configuration to specific classes using @ notation |
| Filler Pattern | Control which properties are dynamically configurable and how they’re serialized |
| FilteredConfiguration | Wraps IConfiguration to apply class-based filtering |
The combination of these features provides a flexible, powerful configuration system that supports: - Environment-specific defaults - Per-request customization for debugging - Hot-switching features without redeploy - Component-level configuration granularity - Safe exposure of only appropriate properties for dynamic override