Calq Config

Calq Config is a preset-driven configuration framework. Designed for error-free, AI-operable configuration management and localization.

Comparison

Feature Calq Config DI Options Pattern Manual File Deserialization External Config Services Environment Variable Libraries
Runtime preset switching ✅ single assignment, auto-reload ❌ restart or rebuild ❌ user responsibility ⚠️ feature flags / label filters ❌ restart required
Coordinated multi-type switching ✅ master preset cascades all types ❌ user responsibility ⚠️ label namespaces
New presets from current state ✅ automatic ❌ user responsibility
Stable object identity across reloads ✅ same instance ⚠️ new instance per change ❌ new instance ⚠️ new snapshot ❌ restart required
Partial update by path ✅ built-in ❌ full rebind ⚠️ manual JSON patching ✅ key-value write
No DI container required ❌ requires service collection ⚠️ SDK often assumes DI
Zero-ceremony setup ✅ auto-resolved directory + POCO defaults ⚠️ builder + bind + registration ✅ one-liner ❌ provisioning + connection
Built-in localization pattern ✅ preset groups per language ❌ separate localization stack ❌ user responsibility ⚠️ label per locale
Change notification ❌ user responsibility
AI-operability ✅ plain classes, self-describing file layout ⚠️ DI registration + section path strings ⚠️ schema not discoverable at runtime ⚠️ cloud API + auth prerequisite ⚠️ flat key-value, no structure
Plain-code model ✅ plain POCO, no attributes ⚠️ no base class, but DI ceremony ✅ plain POCO ⚠️ SDK annotations ✅ flat only
Multi-backend extensibility ✅ custom providers ✅ any serializer ✅ native
Multi-source merge ❌ single source per type ✅ layered providers ❌ user responsibility ✅ layered with fallback ⚠️ env only
Validation ✅ DataAnnotations + custom ❌ user responsibility ⚠️ varies
Secret management ❌ user responsibility ✅ Secret Manager + Key Vault ❌ user responsibility ✅ native ⚠️ insecure at rest

Table of Contents

Usage - Calq Config

1. Foundations

1.1 Registry setup

JsonConfigurationRegistry is the main entry point. It manages a directory of JSON files, one per configuration type per preset.

using CalqFramework.Config.Json;

// Default directory resolution
var registry = new JsonConfigurationRegistry();

// Explicit directory
var registry = new JsonConfigurationRegistry("/path/to/config");

// With preset group orchestration
var registry = new JsonConfigurationRegistry<MasterPreset>("/path/to/config");

Key points:

  • JsonConfigurationRegistry (non-generic) disables preset group logic — all items use the "default" preset unless switched manually
  • JsonConfigurationRegistry<TPreset> enables preset group cascading from a master preset POCO (covered in 5. Preset Groups below)
  • The directory is created automatically if it doesn't exist

1.2 Directory resolution

When no directory is specified, the registry resolves the configuration directory automatically.

Key points:

  • First choice: {AppContext.BaseDirectory}/config (if the directory exists)
  • Fallback: {AppData}/{ProcessName}

See also: 1.1 Registry setup

1.3 File naming convention

Each configuration type is stored as a separate JSON file per preset.

config/
  MyApp.AppSettings.default.json
  MyApp.UiConfig.dark.json
  MyApp.UiConfig.light.json

Key points:

  • Format: {FullTypeName}.{preset}.json
  • Original casing is preserved in JSON output (no camelCase transformation)
  • Case-insensitive matching is used when reading JSON back into the POCO

See also: 1.1 Registry setup

2. Binding

2.1 POCO definition

Configuration types are plain C# classes with a parameterless constructor. Properties and fields with public getters/setters become configuration entries.

class AppSettings {
    public string Host { get; set; } = "localhost";
    public int Port { get; set; } = 8080;
    public bool EnableSsl { get; set; } = false;
    public List<string> AllowedOrigins { get; set; } = new();
}

Key points:

  • Default values in the class definition serve as fallbacks when the JSON file is missing or incomplete

2.2 Supported member types

Key points:

  • Properties with public get/set
  • Public fields
  • Collections: List<T>, HashSet<T>, Dictionary<TKey, TValue>, and other ICollection<T> implementations
  • Any type serializable by Newtonsoft.Json

See also: 2.1 POCO definition

2.3 Collection binding

Collections are cleared and repopulated on reload — the same collection instance is reused.

class AppSettings {
    public List<string> AllowedOrigins { get; set; } = new();
    public Dictionary<string, int> Limits { get; set; } = new();
    public HashSet<string> Tags { get; set; } = new();
}

Key points:

  • List<T>: cleared via Clear(), repopulated via Add()
  • Dictionary<TKey, TValue>: cleared via Clear(), repopulated via indexer
  • HashSet<T> and other ICollection<T>: cleared via Clear(), repopulated via Add()
  • Object identity of the collection is preserved across reloads

See also: 2.1 POCO definition

3. Instance Lifecycle

3.1 Instance identity

The returned object reference is stable — it survives reloads and always reflects the latest state.

var registry = new JsonConfigurationRegistry();
AppSettings settings = await registry.GetAsync<AppSettings>();
AppSettings reference = settings;

// External process edits the JSON file...
await registry.ReloadAsync<AppSettings>();

// Same object, updated values
Assert.Same(reference, settings);
Console.WriteLine(settings.Host); // reflects the file change

Key points:

  • Reload populates the existing POCO instance in-place — object identity is preserved
  • Collections are cleared and repopulated (not replaced) — the same List<T>, Dictionary<TKey, TValue>, or HashSet<T> instance is reused
  • Items removed from the JSON file are removed from the collection; items added are added

See also: 2.3 Collection binding

3.2 Saving

SaveAsync<T>() serializes the current POCO state to its JSON file.

var registry = new JsonConfigurationRegistry();
AppSettings settings = await registry.GetAsync<AppSettings>();
settings.Host = "example.com";
settings.Port = 443;
await registry.SaveAsync<AppSettings>();
// Writes to: {configDir}/MyApp.AppSettings.default.json

See also: 1.3 File naming convention

3.3 Reload from disk

ReloadAsync<T>() re-reads the JSON file and populates the existing POCO instance in-place.

var registry = new JsonConfigurationRegistry();
AppSettings settings = await registry.GetAsync<AppSettings>();

// External process edits the JSON file...
await registry.ReloadAsync<AppSettings>();
Console.WriteLine(settings.Host); // reflects the file change

Key points:

  • Object identity is preserved — the same POCO instance is reused
  • Collection instances are preserved — cleared and repopulated
  • If the JSON file doesn't exist, the POCO retains its current state

See also: 3.1 Instance identity, 2.3 Collection binding

4. Presets

4.1 Preset switching

Setting Preset triggers an automatic reload from the new preset's file.

var item = new JsonConfigurationItem<UiConfig>("/path/to/config", "default");
await item.ReloadAsync();

// Switch to dark preset — reloads from MyApp.UiConfig.dark.json
item.Preset = "dark";
Console.WriteLine(item.Item.DarkMode); // true

See also: 3.3 Reload from disk, 1.3 File naming convention

4.2 Master preset definition

Use JsonConfigurationRegistry<TPreset> with a master preset POCO to orchestrate which preset file is loaded for each configuration type.

class MasterPreset {
    public string Theme { get; set; } = "default";
    public string Region { get; set; } = "default";
}

Key points:

  • The master preset itself always uses the "default" preset
  • Each property on the master preset corresponds to a preset group name (mapped via [PresetGroup] in 5.1 PresetGroup attribute below)

See also: 1.1 Registry setup

4.3 Clone-on-missing behavior

If the target preset file doesn't exist, the current POCO state is saved to it first, then reloaded.

Key points:

  • This creates a new preset file initialized with the current in-memory state
  • Subsequent switches to the same preset load from the newly created file

See also: 4.1 Preset switching, 3.2 Saving

5. Preset Groups

5.1 PresetGroup attribute

[PresetGroup("PropertyName")] maps a configuration type to a property on the master preset POCO.

[PresetGroup("Theme")]
class UiConfig {
    public string Title { get; set; } = "";
    public int FontSize { get; set; }
    public bool DarkMode { get; set; }
}

[PresetGroup("Region")]
class RegionConfig {
    public string Language { get; set; } = "";
    public string Currency { get; set; } = "";
}

File layout:

config/
  MyApp.MasterPreset.default.json    → {"Theme":"dark","Region":"us"}
  MyApp.UiConfig.dark.json           → {"Title":"Dark UI","FontSize":14,"DarkMode":true}
  MyApp.UiConfig.light.json          → {"Title":"Light UI","FontSize":12,"DarkMode":false}
  MyApp.RegionConfig.us.json         → {"Language":"en","Currency":"USD"}
  MyApp.RegionConfig.eu.json         → {"Language":"de","Currency":"EUR"}

Usage:

var registry = new JsonConfigurationRegistry<MasterPreset>("/path/to/config");

UiConfig ui = await registry.GetAsync<UiConfig>();
// Loaded from UiConfig.dark.json (because MasterPreset.Theme == "dark")

RegionConfig region = await registry.GetAsync<RegionConfig>();
// Loaded from RegionConfig.us.json (because MasterPreset.Region == "us")

Key points:

  • Configuration types without [PresetGroup] always use the "default" preset

See also: 4.2 Master preset definition, 4.1 Preset switching, 1.3 File naming convention

5.2 Cascading reloads

ReloadAllAsync() reloads the master preset first, then cascades to all child items — switching their preset files if the master's values changed.

// External edit changes MasterPreset.default.json: Theme = "light"
await registry.ReloadAllAsync();

// ui is now populated from UiConfig.light.json
Console.WriteLine(ui.Title);    // "Light UI"
Console.WriteLine(ui.DarkMode); // false

Key points:

  • If a child's resolved preset changed, it switches automatically
  • If a child's resolved preset is unchanged, it reloads from the same file

See also: 5.1 PresetGroup attribute, 4.1 Preset switching, 3.3 Reload from disk

6. Preset Variants & Discovery

6.1 Named variants

Each configuration type can have multiple named presets, stored as separate JSON files. The default preset is "default".

config/
  MyApp.UiConfig.default.json
  MyApp.UiConfig.dark.json
  MyApp.UiConfig.light.json

See also: 1.3 File naming convention

6.2 Preset discovery

Available presets are discovered by globbing {TypeName}.*.json in the config directory.

var item = new JsonConfigurationItem<UiConfig>("/path/to/config", "default");
IEnumerable<string> presets = item.AvailablePresets;
// ["default", "dark", "light"]

See also: 6.1 Named variants, 1.3 File naming convention

6.3 Querying preset groups

IEnumerable<string> groups = registry.AvailablePresetGroups;
// ["Theme", "Region"]

IEnumerable<string> themePresets = registry.GetAvailablePresets("Theme");
// ["dark", "light"]

See also: 6.2 Preset discovery, 5.1 PresetGroup attribute

7. Change Notification

7.1 Reload events

var item = new JsonConfigurationItem<AppSettings>("/path/to/config", "default");
item.OnReloaded += () => Console.WriteLine("Config reloaded");
await item.ReloadAsync();

Key points:

  • OnReloaded fires after every successful reload, including preset switches
  • Subscribe before the first ReloadAsync() call to receive all notifications

See also: 3.3 Reload from disk, 4.1 Preset switching, 5.2 Cascading reloads

8. Configuration Store

8.1 Loading and access

GetAsync<T>() loads a configuration item. The first call deserializes from disk; subsequent calls return the same instance.

var registry = new JsonConfigurationRegistry();
AppSettings settings = await registry.GetAsync<AppSettings>();
Console.WriteLine(settings.Host); // "localhost" or value from JSON file

// Same instance every time
AppSettings same = await registry.GetAsync<AppSettings>();
Assert.Same(settings, same);

Key points:

  • LoadAsync<T>() is an alias for GetAsync<T>() — both return the same singleton instance
  • If no JSON file exists, the POCO retains its default values
  • Thread-safe via ConcurrentDictionary internally

See also: 2.1 POCO definition, 3.1 Instance identity, 4.1 Preset switching

9. Partial Updates

9.1 Set by path

SetByPathAsync modifies a single value in the JSON file by dot-separated path, then reloads the POCO to stay in sync.

var registry = new JsonConfigurationRegistry();
AppSettings settings = await registry.GetAsync<AppSettings>();

// Update a single value — writes to file and reloads
await registry.SetByPathAsync<AppSettings>("Host", "example.com");
await registry.SetByPathAsync<AppSettings>("Port", "443");

Console.WriteLine(settings.Host); // "example.com"
Console.WriteLine(settings.Port); // 443

Key points:

  • The POCO is reloaded after the file write to stay in sync
  • Useful for CLI tools or APIs that need to set individual config values without loading the full object

See also: 3.3 Reload from disk, 3.2 Saving

9.2 Type preservation

Key points:

  • If the existing JSON value is an integer, the new string value is parsed as integer
  • If the existing JSON value is a float, the new string value is parsed as float
  • If the existing JSON value is a boolean, the new string value is parsed as boolean
  • If the path doesn't exist or the value is a string, the value is stored as-is

See also: 9.1 Set by path

9.3 Nested paths

await registry.SetByPathAsync<AppSettings>("Nested.DeepValue", "42");
// Creates intermediate objects if they don't exist in the JSON

Key points:

  • Missing path segments are created automatically as JSON objects

See also: 9.1 Set by path, 2.1 POCO definition

10. Localization Pattern

The same preset group mechanism supports localization: tag text classes with [PresetGroup("Language")], create one JSON file per language, and switch all translations by changing a single master preset value.

class MasterPreset {
    public string Language { get; set; } = "en";
}

[PresetGroup("Language")]
class HomePageText {
    public string WelcomeMessage { get; set; } = "Welcome";
    public string SignIn { get; set; } = "Sign In";
}

[PresetGroup("Language")]
class SharedText {
    public string NavHome { get; set; } = "Home";
    public string NavSettings { get; set; } = "Settings";
}
config/
  MyApp.MasterPreset.default.json    → {"Language":"en"}
  MyApp.HomePageText.en.json         → {"WelcomeMessage":"Welcome","SignIn":"Sign In"}
  MyApp.HomePageText.es.json         → {"WelcomeMessage":"Bienvenido","SignIn":"Iniciar sesión"}
  MyApp.SharedText.en.json           → {"NavHome":"Home","NavSettings":"Settings"}
  MyApp.SharedText.es.json           → {"NavHome":"Inicio","NavSettings":"Configuración"}
var registry = new JsonConfigurationRegistry<MasterPreset>("/path/to/config");

HomePageText home = await registry.GetAsync<HomePageText>();
SharedText shared = await registry.GetAsync<SharedText>();

Console.WriteLine(home.WelcomeMessage); // "Welcome"
Console.WriteLine(shared.NavHome);      // "Home"

// Switch language — all text classes reload automatically
await registry.SetByPathAsync<MasterPreset>("Language", "es");
await registry.ReloadAllAsync();

Console.WriteLine(home.WelcomeMessage); // "Bienvenido"
Console.WriteLine(shared.NavHome);      // "Inicio"

Key points:

  • Best for single-user applications (desktop, games, CLI tools) where one language is active at a time
  • Switching the master preset reloads all text classes in one operation

See also: 5.1 PresetGroup attribute, 5.2 Cascading reloads, 9.1 Set by path

10.1 Multi-user localization (web apps)

For multi-user web applications that serve multiple languages simultaneously, use standalone JsonConfigurationItem<T> instances — one per text class per language — without the registry or [PresetGroup].

class HomePageText {
    public string WelcomeMessage { get; set; } = "Welcome";
    public string SignIn { get; set; } = "Sign In";
}

class SharedText {
    public string NavHome { get; set; } = "Home";
    public string NavSettings { get; set; } = "Settings";
}
config/
  MyApp.HomePageText.ja.json         → {"WelcomeMessage":"ようこそ","SignIn":"ログイン"}
  MyApp.SharedText.ja.json           → {"NavHome":"ホーム","NavSettings":"設定"}
using CalqFramework.Config.Json;

string[] supportedLanguages = ["en", "ja"];
var texts = new Dictionary<string, HomePageText>();

foreach (var lang in supportedLanguages) {
    var item = new JsonConfigurationItem<HomePageText>("/path/to/config", lang);
    await item.ReloadAsync();
    texts[lang] = item.Item;
}

// English uses POCO defaults (no JSON file needed)
Console.WriteLine(texts["en"].WelcomeMessage); // "Welcome"

// Japanese loaded from HomePageText.ja.json
Console.WriteLine(texts["ja"].WelcomeMessage); // "ようこそ"

Key points:

  • No registry, no MasterPreset, no [PresetGroup] attribute needed
  • English defaults live in the POCO class — no JSON file required
  • Each language is loaded once at startup and looked up per-request
  • Best for web servers where multiple users have different language preferences simultaneously

See also: 4.1 Preset switching, 2.1 POCO definition, 1.3 File naming convention

11. Extensibility

11.1 Custom configuration item

The JSON implementation is one backend. Create others (database, remote API, YAML, etc.) by extending ConfigurationItemBase<T>.

using CalqFramework.Config;

class DatabaseConfigurationItem<TItem> : ConfigurationItemBase<TItem> where TItem : class, new() {
    private readonly string _connectionString;

    public DatabaseConfigurationItem(string connectionString, string preset) : base(preset) {
        _connectionString = connectionString;
    }

    public override IEnumerable<string> AvailablePresets {
        get {
            return QueryPresets(_connectionString, typeof(TItem).FullName!);
        }
    }

    protected override async Task ReloadAsync(string preset) {
        string json = await ReadFromDatabase(_connectionString, typeof(TItem).FullName!, preset);
        // Deserialize json into Item (populate properties/fields)
        RaiseOnReloaded();
    }

    public override async Task SaveAsync() {
        string json = SerializeItem();
        await WriteToDatabase(_connectionString, typeof(TItem).FullName!, Preset, json);
    }

    public override async Task SetByPathAsync(string jsonPath, string value) {
        // Implement partial update logic for your backend
        await Task.CompletedTask;
    }

    protected override bool PresetExists(string preset) {
        return CheckPresetExists(_connectionString, typeof(TItem).FullName!, preset);
    }
}

Key points:

  • ConfigurationItemBase<T> handles preset group attribute caching, preset switching logic, and the OnReloaded event
  • Your backend only needs to implement: ReloadAsync, SaveAsync, SetByPathAsync, AvailablePresets, PresetExists
  • Call RaiseOnReloaded() at the end of ReloadAsync to fire the OnReloaded event

See also: 7.1 Reload events, 9.1 Set by path, 4.1 Preset switching

11.2 Custom configuration registry

Extend ConfigurationRegistryBase<T> to provide the registry orchestration for your custom backend.

class DatabaseConfigurationRegistry<TPreset> : ConfigurationRegistryBase<TPreset> where TPreset : class, new() {
    private readonly string _connectionString;

    public DatabaseConfigurationRegistry(string connectionString) {
        _connectionString = connectionString;
        Initialize(); // required — bootstraps the master preset item
    }

    protected override IConfigurationItem<TItem> CreateItem<TItem>(string preset) =>
        new DatabaseConfigurationItem<TItem>(_connectionString, preset);
}

Key points:

  • ConfigurationRegistryBase<T> handles the item dictionary, master preset cascading, and ReloadAllAsync orchestration
  • Call Initialize() in your registry constructor — it creates the master preset item
  • Override CreateItem<TItem> to return your custom ConfigurationItemBase<T> implementation

See also: 11.1 Custom configuration item, 5.2 Cascading reloads, 1.1 Registry setup

Quick Start

dotnet new console -n QuickStart
cd QuickStart
dotnet add package CalqFramework.Config

Replace Program.cs with:

using CalqFramework.Config.Json;

var registry = new JsonConfigurationRegistry();
var settings = await registry.GetAsync<AppSettings>();
Console.WriteLine($"Host: {settings.Host}, Port: {settings.Port}");

settings.Host = "example.com";
settings.Port = 443;
await registry.SaveAsync<AppSettings>();
Console.WriteLine($"Host: {settings.Host}, Port: {settings.Port}");

class AppSettings {
    public string Host { get; set; } = "localhost";
    public int Port { get; set; } = 8080;
    public bool EnableSsl { get; set; } = false;
}
dotnet run

License

Calq Config is dual-licensed under PolyForm Noncommercial (with Evaluation Grant) and the Calq Commercial License.

An unhandled error has occurred. Reload 🗙