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
- 2. Binding
- 3. Instance Lifecycle
- 4. Presets
- 5. Preset Groups
- 6. Preset Variants & Discovery
- 7. Change Notification
- 8. Configuration Store
- 9. Partial Updates
- 10. Localization Pattern
- 11. Extensibility
- Quick Start
- License
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 manuallyJsonConfigurationRegistry<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 otherICollection<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 viaClear(), repopulated viaAdd()Dictionary<TKey, TValue>: cleared viaClear(), repopulated via indexerHashSet<T>and otherICollection<T>: cleared viaClear(), repopulated viaAdd()- 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>, orHashSet<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:
OnReloadedfires 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 forGetAsync<T>()— both return the same singleton instance- If no JSON file exists, the POCO retains its default values
- Thread-safe via
ConcurrentDictionaryinternally
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 theOnReloadedevent- Your backend only needs to implement:
ReloadAsync,SaveAsync,SetByPathAsync,AvailablePresets,PresetExists - Call
RaiseOnReloaded()at the end ofReloadAsyncto fire theOnReloadedevent
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, andReloadAllAsyncorchestration- Call
Initialize()in your registry constructor — it creates the master preset item - Override
CreateItem<TItem>to return your customConfigurationItemBase<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.