NextIteration.SpectreConsole.Settings 0.1.0

dotnet add package NextIteration.SpectreConsole.Settings --version 0.1.0
                    
NuGet\Install-Package NextIteration.SpectreConsole.Settings -Version 0.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="NextIteration.SpectreConsole.Settings" Version="0.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="NextIteration.SpectreConsole.Settings" Version="0.1.0" />
                    
Directory.Packages.props
<PackageReference Include="NextIteration.SpectreConsole.Settings" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add NextIteration.SpectreConsole.Settings --version 0.1.0
                    
#r "nuget: NextIteration.SpectreConsole.Settings, 0.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package NextIteration.SpectreConsole.Settings@0.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=NextIteration.SpectreConsole.Settings&version=0.1.0
                    
Install as a Cake Addin
#tool nuget:?package=NextIteration.SpectreConsole.Settings&version=0.1.0
                    
Install as a Cake Tool

NextIteration.SpectreConsole.Settings

NuGet Downloads License: MIT .NET CI

Strongly-typed, JSON-persisted settings and ready-made settings commands for CLI tools built on Spectre.Console.

Stop hand-rolling the same ~/.app/config.json + load/save boilerplate into every CLI you build. Declare a settings class, register it, and inject it into any command. Change a property and it's persisted — automatically and debounced, or explicitly when you say so — and my-cli settings list / reset just work.


Features

  • Strongly-typed settings — derive from SettingsBase, back each property with a field, call OnPropertyChanged(). That's the whole contract.
  • Automatic or explicit persistenceAutomatic mode debounces a burst of changes into a single async disk write; Explicit mode persists only when you call Save() / SaveAsync().
  • settings command branchlist and reset wired into your existing CommandApp with a single call.
  • Atomic writes — a crash mid-write never leaves a half-written settings file on disk.
  • Tolerant deserialisation — added a property? It defaults. Removed one? The old key is ignored. No migration ceremony for ordinary schema evolution.
  • Errors are never swallowed — a failed fire-and-forget write is routed to a configurable error handler (default: one line to stderr).
  • Zero compiler warnings, fully documented public surface<GenerateDocumentationFile> on, analyzers on, TreatWarningsAsErrors on.

Install

dotnet add package NextIteration.SpectreConsole.Settings

Targets net10.0.


Quick start

1. Declare a settings class

Derive from SettingsBase, back each property with a field, and call OnPropertyChanged() from the setter:

using NextIteration.SpectreConsole.Settings;

public sealed class AppSettings : SettingsBase
{
    private DataSourceMode _mode = DataSourceMode.File;

    public DataSourceMode Mode
    {
        get => _mode;
        set
        {
            _mode = value;
            OnPropertyChanged();
        }
    }
}

public enum DataSourceMode { File, Api }

2. Register it

One call per settings class. SettingsDirectory is required — there's no smart default:

using NextIteration.SpectreConsole.Settings;

services.AddSettings<AppSettings>(opts =>
{
    opts.SettingsDirectory = Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
        ".my-cli", "settings");
});

// A second class — this one persists only on explicit Save():
services.AddSettings<CacheSettings>(opts =>
{
    opts.SettingsDirectory = Path.Combine(
        Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
        ".my-cli", "settings");
    opts.PersistenceMode = PersistenceMode.Explicit;
});

3. Hook the settings branch into your configurator

app.Configure(config =>
{
    config.AddSettingsBranch();
    // ... your other commands
});

4. Inject and use it from any command

public sealed class SwitchModeCommand(AppSettings settings) : AsyncCommand
{
    public override async Task<int> ExecuteAsync(CommandContext context)
    {
        settings.Mode = DataSourceMode.Api;
        // No explicit Save() needed — persisted automatically (debounced).
        return 0;
    }
}

That's it. Each settings class is serialised to its own JSON file in the directory, named after the class (e.g. AppSettings.json). On the next launch it's deserialised and handed to your commands.


The settings branch

Command Description
settings list Table of every registered settings class and its current property values.
settings reset <SettingsClassName> Reset one settings class to default values and persist.
settings reset --all Reset every registered settings class to defaults and persist.

Every command accepts -v / --verbose for full stack-trace output when something goes wrong.

$ my-cli settings list
AppSettings (Automatic)
/home/me/.my-cli/settings/AppSettings.json
┌──────────┬──────┐
│ Property │ Value│
├──────────┼──────┤
│ Mode     │ Api  │
└──────────┴──────┘

$ my-cli settings reset AppSettings
Reset 'AppSettings' to defaults? This overwrites the saved file and cannot be undone. [y/N]: y
Reset 'AppSettings' to defaults.

reset prompts for confirmation (defaulting to "no") before overwriting. Pass -f / --force to skip the prompt in scripts or CI.


Persistence model

Automatic (default)

Every OnPropertyChanged() schedules a debounced asynchronous write. A burst of changes in the same synchronous call stack — or within the debounce window — coalesces into a single disk write, so setting five properties in a row touches the file once. The default window is 250 ms; tune it per class via SettingsOptions.DebounceInterval.

Writes are fire-and-forget, but failures are never silently swallowed — they're routed to SettingsOptions.ErrorHandler (default: a single line to stderr).

Explicit

OnPropertyChanged() becomes a no-op; nothing is written until you call Save() or SaveAsync().

  • Save() — fire-and-forget. Convenient, but in a command that mutates-then-returns the process may exit before the write lands.
  • SaveAsync() — awaitable. Use it when you need the write to be on disk before the command returns. Exceptions propagate to the awaiter rather than the error handler.

Both methods work in either mode — call SaveAsync() in Automatic mode too when you want to flush deterministically.

Loading

On startup each registered class is deserialised from its JSON file. If the file doesn't exist, a default-constructed instance is used — no exception. Property assignments performed by the deserializer during load never trigger a write; persistence only kicks in for changes you make afterwards.


Schema evolution

Settings classes change over time. The default serializer is tolerant:

  • Added a property? Files written by older versions simply lack the key, so it takes its constructed default.
  • Removed a property? The now-unknown key in older files is ignored.
  • Property matching is case-insensitive; enums round-trip as readable strings; comments and trailing commas are tolerated on read.

Supply your own SettingsOptions.SerializerOptions if you need different behaviour.


Nested values & keeping it simple

Persistence handles whatever System.Text.Json can serialise — nested objects, lists, and dictionaries all round-trip to and from the JSON file without extra work. Two things to know once you go beyond scalars:

  • Automatic save only fires on the top-level setter. OnPropertyChanged() runs when you assign a property on the settings object — not when you mutate inside a nested object or collection:

    settings.Profile = new Profile { Name = "Ada" };   // ✅ persisted (the setter ran)
    settings.Profile.Name = "Ada";                     // ❌ not detected in Automatic mode
    settings.RecentFiles.Add("notes.txt");             // ❌ not detected
    

    To persist a nested change: reassign the whole property, model nested values as immutable records and swap them (settings.Profile = settings.Profile with { Name = "Ada" };), or call settings.Save() / await settings.SaveAsync() after mutating.

  • settings list renders complex values as compact JSON, so the table stays readable instead of printing a type name.

Recommendation: prefer scalar properties

Keep your life simple — make settings properties scalars (string, bool, numbers, enum, DateTime, Guid, Uri, …) wherever you can. A flat scalar settings class never hits the in-place-mutation gotcha above, reads cleanly in settings list, and is trivial to reason about. Reach for a nested object or collection only when you're happy to assign it wholesale.


Resetting at runtime

ISettingsStore is registered as a singleton and aggregates every class you registered. It powers settings reset, and you can use it directly:

public sealed class FactoryResetCommand(ISettingsStore store) : AsyncCommand
{
    public override async Task<int> ExecuteAsync(CommandContext context)
    {
        await store.ResetAllAsync();
        return 0;
    }
}

A reset restores defaults in place on the live instance and persists immediately — any command that injected the settings object directly observes the change without a restart.


Configuration reference

AddSettings<T>(opts => …) accepts a SettingsOptions:

Option Default Notes
SettingsDirectory — (required) Absolute path; throws at registration if unset.
PersistenceMode Automatic Automatic (debounced) or Explicit.
DebounceInterval 250 ms Coalescing window for automatic writes.
ErrorHandler stderr line Invoked when a fire-and-forget write fails.
SerializerOptions tolerant default Override the JsonSerializerOptions.

T must derive from SettingsBase and have a public parameterless constructor (used for defaults and by the deserializer).


Requirements

  • .NET 10.0 or later
  • Spectre.Console 0.54+ and Spectre.Console.Cli 0.53+
  • Microsoft.Extensions.DependencyInjection.Abstractions 10.0+

Everything else is transitive.


Contributing

Issues and PRs welcome.

When contributing code, please keep the zero-warning, fully-documented public surface. TreatWarningsAsErrors is on for a reason.


Changelog

See CHANGELOG.md for release notes.


License

MIT © Stuart Meeks

Built for — and unaffiliated with — the excellent Spectre.Console project.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0 209 5/27/2026