Registry — Persistent Key-Value Storage
The registry is a hierarchical persistent key-value store built into SharpConsoleUI. It survives application restarts and is designed for storing user preferences, window state, and application settings. The backing store is a JSON file by default, with a pluggable storage interface for custom backends.
Table of Contents
- Overview
- Quick Start
- Configuration
- Reading and Writing Values
- Sections and Paths
- Key Management
- Thread Safety
- Custom Storage Backends
- RegistryStateService
- Complete Example
Overview
The registry organizes data into a tree of sections, each containing key-value pairs. Sections map to nested JSON objects on disk. Paths use / as the separator:
app/
ui/
theme = "ModernGray"
windowWidth = 120
preferences/
autoSave = true
lastFile = "/home/user/document.txt"
The registry is backed by AppRegistry, which is exposed through ConsoleWindowSystem.RegistryStateService after being initialized with a RegistryConfiguration. It loads on startup and saves on shutdown automatically.
When no file path is specified, RegistryConfiguration.Default resolves to a platform-appropriate location derived from the process name:
| Platform | Path |
|---|---|
| Windows | %APPDATA%\<processname>\registry.json |
| Linux/macOS | ~/.config/<processname>/registry.json |
Quick Start
Pass a RegistryConfiguration when constructing ConsoleWindowSystem:
var windowSystem = new ConsoleWindowSystem(
new NetConsoleDriver(RenderMode.Buffer),
registryConfiguration: RegistryConfiguration.ForFile("myapp.json")
);
// Access the registry through RegistryStateService
var registry = windowSystem.RegistryStateService;
// Open a section (created automatically if it doesn't exist)
var prefs = registry.OpenSection("app/preferences");
// Read a value (returns default if key is absent)
string theme = prefs.GetString("theme", "ModernGray");
bool autoSave = prefs.GetBool("autoSave", true);
// Write a value
prefs.SetString("theme", "Solarized");
prefs.SetBool("autoSave", false);
// Registry saves automatically on windowSystem.Dispose() / end of Run()
Configuration
RegistryConfiguration
public record RegistryConfiguration(
string FilePath = "registry.json",
bool EagerFlush = false,
TimeSpan? FlushInterval = null,
IRegistryStorage? Storage = null
)
| Parameter | Type | Default | Description |
|---|---|---|---|
FilePath |
string |
"registry.json" |
Path to the JSON file. Relative paths resolve from the working directory. |
EagerFlush |
bool |
false |
Write to disk on every Set* call. |
FlushInterval |
TimeSpan? |
null |
Background timer flush interval. null disables timer flushing. |
Storage |
IRegistryStorage? |
null |
Custom storage backend. When null, uses JsonFileStorage with FilePath. |
Static factory helpers
// Default — saves to a platform-appropriate path, manual flush only:
// Windows: %APPDATA%\<processname>\registry.json
// Linux/macOS: ~/.config/<processname>/registry.json
var config = RegistryConfiguration.Default;
// Save to a specific file path
var config = RegistryConfiguration.ForFile("data/settings.json");
// Eager flush — every Set writes immediately
var config = new RegistryConfiguration(EagerFlush: true);
// Timer-based flush every 30 seconds
var config = new RegistryConfiguration(FlushInterval: TimeSpan.FromSeconds(30));
Flush Modes
The registry does not write to disk automatically on every change by default. Choose a flush strategy based on your needs:
| Mode | How to enable | When it writes |
|---|---|---|
| Manual (default) | RegistryConfiguration.Default |
Only on explicit Save() or application shutdown |
| Eager | EagerFlush: true |
After every Set* call |
| Timer | FlushInterval: TimeSpan.FromSeconds(N) |
Every N seconds in the background |
| Shutdown | Always active | RegistryStateService.Dispose() — called by ConsoleWindowSystem on exit |
Modes can be combined: eager + timer is valid (though redundant). Shutdown-save always runs regardless of mode.
Reading and Writing Values
All read/write operations are performed through a RegistrySection obtained via OpenSection().
Primitive Types
var section = registry.OpenSection("app/ui");
// string
section.SetString("theme", "ModernGray");
string theme = section.GetString("theme", "ModernGray"); // default = ""
// int
section.SetInt("windowWidth", 120);
int width = section.GetInt("windowWidth", 80); // default = 0
// bool
section.SetBool("showToolbar", true);
bool show = section.GetBool("showToolbar", true); // default = false
// double
section.SetDouble("opacity", 0.95);
double opacity = section.GetDouble("opacity", 1.0); // default = 0.0
// DateTime (stored as ISO 8601)
section.SetDateTime("lastOpened", DateTime.UtcNow);
DateTime dt = section.GetDateTime("lastOpened", DateTime.MinValue);
All Get* methods return the defaultValue parameter if the key is absent or has an incompatible type — they never throw.
Generic Types (AOT-safe)
For complex types, use the JsonTypeInfo<T> overloads. These are AOT-safe and work with source generation:
// Define a source-generated context
[JsonSerializable(typeof(WindowPosition))]
public partial class AppJsonContext : JsonSerializerContext { }
// Usage
var pos = new WindowPosition { X = 10, Y = 20, Width = 80, Height = 24 };
section.Set("mainWindowPos", pos, AppJsonContext.Default.WindowPosition);
var loaded = section.Get("mainWindowPos", new WindowPosition(),
AppJsonContext.Default.WindowPosition);
Sections and Paths
Sections are hierarchical nodes in the registry tree. OpenSection() creates any missing intermediate nodes automatically.
// Open a deeply nested section
var section = registry.OpenSection("app/windows/main/layout");
// "/" is the separator; leading and trailing slashes are trimmed
var same = registry.OpenSection("/app/windows/main/layout/");
// Empty path returns the root section
var root = registry.OpenSection("");
// Sections can be opened from another section (relative navigation)
var app = registry.OpenSection("app");
var ui = app.OpenSection("ui"); // equivalent to "app/ui"
var prefs = app.OpenSection("preferences"); // equivalent to "app/preferences"
Path rules:
/is the path separator- Leading and trailing slashes are ignored
- Empty segments (e.g.
"a//b") throwArgumentException - Empty path or
"/"returns the current section (root if called on the registry)
Key Management
var section = registry.OpenSection("app/ui");
// Check if a key exists
bool hasTheme = section.HasKey("theme");
// Get all leaf value keys in this section (not sub-sections)
IReadOnlyList<string> keys = section.GetKeys();
// Get all direct child section names
IReadOnlyList<string> subSections = section.GetSubSectionNames();
// Delete a key
section.DeleteKey("theme");
// Delete a sub-section and all its contents
section.DeleteSection("oldSettings");
Thread Safety
AppRegistry.OpenSection(), Save(), and Load() are thread-safe via ReaderWriterLockSlim.
RegistrySection instances are NOT thread-safe. Do not share a single RegistrySection across threads. Instead, call OpenSection() per thread to get an independent section view:
// Safe — each thread opens its own section instance
Task.Run(() =>
{
var section = registry.OpenSection("app/preferences");
section.SetString("key", "value");
});
// Not safe — sharing one section instance across threads
var shared = registry.OpenSection("app/preferences");
Task.Run(() => shared.SetString("key", "value")); // race condition
Custom Storage Backends
Implement IRegistryStorage to use any storage medium:
public interface IRegistryStorage
{
void Save(JsonNode root);
JsonNode? Load();
}
Example — encrypted file storage:
public class EncryptedFileStorage : IRegistryStorage
{
private readonly string _path;
private readonly byte[] _key;
public EncryptedFileStorage(string path, byte[] key)
{
_path = path;
_key = key;
}
public void Save(JsonNode root)
{
var json = root.ToJsonString();
var encrypted = Encrypt(json, _key);
File.WriteAllBytes(_path, encrypted);
}
public JsonNode? Load()
{
if (!File.Exists(_path)) return null;
var encrypted = File.ReadAllBytes(_path);
var json = Decrypt(encrypted, _key);
return JsonNode.Parse(json);
}
// ... Encrypt / Decrypt implementation
}
// Use it
var config = new RegistryConfiguration(
Storage: new EncryptedFileStorage("settings.enc", myKey)
);
The built-in MemoryStorage is available for testing — it stores data in memory only, with no file I/O:
var config = new RegistryConfiguration(Storage: new MemoryStorage());
RegistryStateService
RegistryStateService is the lifecycle wrapper that integrates AppRegistry with ConsoleWindowSystem. It:
- Calls
Load()during system initialization - Calls
Save()onDispose()(i.e., whenConsoleWindowSystemshuts down) - Exposes
OpenSection(),Save(), andLoad()directly — no need to unwrap an inner object
Accessed via:
RegistryStateService? registry = windowSystem.RegistryStateService;
RegistryStateService is null if no RegistryConfiguration was passed to ConsoleWindowSystem. Always null-check before use:
var section = windowSystem.RegistryStateService?.OpenSection("app/ui");
section?.SetString("theme", "Solarized");
Manual save
Call Save() explicitly at any point — for example, right after the user changes a setting:
prefs.SetString("theme", selectedTheme);
windowSystem.RegistryStateService?.Save(); // flush to disk now
Manual reload
// Reload from disk — discards any unsaved in-memory changes
windowSystem.RegistryStateService?.Load();
Complete Example
using SharpConsoleUI;
using SharpConsoleUI.Configuration;
using SharpConsoleUI.Drivers;
// Initialize with registry support
var windowSystem = new ConsoleWindowSystem(
new NetConsoleDriver(RenderMode.Buffer),
registryConfiguration: new RegistryConfiguration(
FilePath: "myapp-settings.json",
FlushInterval: TimeSpan.FromMinutes(1) // auto-flush every minute
)
);
var registry = windowSystem.RegistryStateService!;
// Restore window position from last run
var windowPrefs = registry.OpenSection("app/windows/main");
int lastX = windowPrefs.GetInt("x", 5);
int lastY = windowPrefs.GetInt("y", 3);
int lastWidth = windowPrefs.GetInt("width", 80);
int lastHeight = windowPrefs.GetInt("height", 24);
var mainWindow = new WindowBuilder(windowSystem)
.WithTitle("My App")
.WithPosition(lastX, lastY)
.WithSize(lastWidth, lastHeight)
.Build();
// Save position when window moves
mainWindow.Moved += (s, e) =>
{
windowPrefs.SetInt("x", mainWindow.X);
windowPrefs.SetInt("y", mainWindow.Y);
};
mainWindow.Resized += (s, e) =>
{
windowPrefs.SetInt("width", mainWindow.Width);
windowPrefs.SetInt("height", mainWindow.Height);
};
// User preferences
var prefs = registry.OpenSection("app/preferences");
string theme = prefs.GetString("theme", "ModernGray");
windowSystem.ThemeRegistry.SetTheme(theme);
windowSystem.ThemeStateService.ThemeChanged += (s, e) =>
{
prefs.SetString("theme", e.NewTheme.Name);
};
windowSystem.AddWindow(mainWindow);
windowSystem.Run();
// Registry saves automatically here (Dispose → RegistryStateService.Dispose → Save)
See Also
- Configuration — System-level configuration options
- State Services — All built-in state services