ComposableSettings 1.0.13

dotnet add package ComposableSettings --version 1.0.13
                    
NuGet\Install-Package ComposableSettings -Version 1.0.13
                    
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="ComposableSettings" Version="1.0.13" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ComposableSettings" Version="1.0.13" />
                    
Directory.Packages.props
<PackageReference Include="ComposableSettings" />
                    
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 ComposableSettings --version 1.0.13
                    
#r "nuget: ComposableSettings, 1.0.13"
                    
#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 ComposableSettings@1.0.13
                    
#: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=ComposableSettings&version=1.0.13
                    
Install as a Cake Addin
#tool nuget:?package=ComposableSettings&version=1.0.13
                    
Install as a Cake Tool

ComposableSettings

Observable, auto-persisting application settings with source-generated boilerplate.

Each settings model declares itself; a source generator gives it INotifyPropertyChanged; a provider hands your code one live instance that auto-persists on every change — including edits nested arbitrarily deep. Different owners (e.g. runtime vs gui) keep their settings in separate files, registered independently.

Why

Traditional settings management funnels everything through a central registry that must know about every settings type — adding one setting means touching the model, a snapshot DTO, an UpdateXAsync method, a path constant, and DI wiring.

ComposableSettings inverts that: a model owns its shape, the generator owns the plumbing, and a consumer just edits a live object.

// No Save(), no Update method, no snapshot rebuild:
clock.Current.BaseColor = "#00FF00";   // persisted automatically; bindings update live

Install

<PackageReference Include="ComposableSettings" Version="1.0.*" />

Targets net10.0. The source generators ship inside the package as analyzers, so they activate automatically on install — no extra reference needed.

Versioning follows CI run numbers (1.0.{run}) on pushes to main.

Quick start

1. Declare settings models

Mark a partial class [SettingsModel] and write private _camelCase fields. The generator emits INotifyPropertyChanged and a public property per field (defaults come from the field initializers).

using System.Collections.ObjectModel;
using ComposableSettings;

[SettingsModel]
public partial class ClockSettings
{
    private string _baseColor = "#e6194b";   // -> public string BaseColor { get; set; }
    private double _brightness = 0.8;          // -> public double Brightness { get; set; }
    private GlowSettings _glow = new();        // nested model (tracked deeply)
}

[SettingsModel]
public partial class GlowSettings
{
    private int _waveFrequency = 24;
    private double _glowIntensity = 0.18;
}

[SettingsModel]
public partial class RuntimeSettings
{
    private string _pluginsFolder = "./plugins";
    private ObservableCollection<string> _enabledPlugins = new();   // observable collection
}

Field kinds the generator understands:

Field type Generated member Notes
scalar — string, value type, enum change-raising property default from initializer
ObservableCollection<T> get-only property add/remove and item edits tracked; readonly field is fine
other reference type (nested model) property with a re-tracking setter field must be non-readonly

2. Register one file per owner + a provider per node

using ComposableSettings;
using ComposableSettings.Runtime; // SettingsNodePath

services.AddComposableSettingsFile("gui", guiXmlPath);        // owner: gui
services.AddSettingsProvider<ClockSettings>("gui", SettingsNodePath.Root("clock"));

services.AddComposableSettingsFile("runtime", runtimeXmlPath); // owner: runtime
services.AddSettingsProvider<RuntimeSettings>("runtime", SettingsNodePath.Root("runtime"));

gui.xml and runtime.xml are independent: the runtime owner does not know about GUI settings and vice versa.

For slider-heavy screens, debounce only the persistence write while keeping the live settings object synchronous:

services.AddSettingsProvider<ClockSettings>(
    "gui",
    SettingsNodePath.Root("clock"),
    TimeSpan.FromMilliseconds(250));

3. Consume the live instance

Inject ISettingsProvider<T> and read/write Current:

public sealed class Engine(ISettingsProvider<RuntimeSettings> settings)
{
    public void Run()
    {
        var folder = settings.Current.PluginsFolder;     // read
        settings.Current.EnabledPlugins.Add("Plugin.A"); // mutate -> auto-persists
    }
}
public interface ISettingsProvider<TSettings>
    where TSettings : class, INotifyPropertyChanged, new()
{
    TSettings Current { get; }              // the live, observable instance
    event EventHandler<TSettings>? Replaced; // raised on Reset/Reload
    void Reset();                            // restore defaults (persisted)
    void Reload();                           // re-read from the backing file
}

There is no Save: the provider subscribes to the instance's PropertyChanged and persists each change. Use the debounced registration overload to coalesce writes to the backing store without delaying Current.

4. (UI) Consumers that bind, with zero stored state

For ViewModels that bind a settings model, mark them [SettingsConsumer(typeof(T))] and call the generated InitializeGeneratedSettings from your own constructor:

[SettingsConsumer(typeof(ClockSettings))]
public partial class ClockViewModel
{
    public ClockViewModel(ISettingsProvider<ClockSettings> settings /*, other deps */)
        => InitializeGeneratedSettings(settings);
    // generated: `public ClockSettings Settings => provider.Current;` + INPC relay
}

Bind to Settings.BaseColor, Settings.Glow.GlowIntensity, … — edits flow straight through to disk, and external resets refresh the binding.

Deep observability

A change at any depth propagates up to the owning model's PropertyChanged, so the provider persists it:

gui.Current.Clock.Glow.GlowIntensity = 0.5;   // grandchild edit -> persisted
palette.Current.Colors.Add("#123456");         // collection add -> persisted
schedules.Current.Jobs[0].Cron = "*/5 * * * *"; // item edit -> persisted (items are [SettingsModel])

This is wired by the generated constructor via SettingsChangeTracking (collections re-sync item subscriptions across add/remove/replace/clear).

Persistence / custom stores

AddComposableSettingsFile(key, path) uses the built-in XmlSettingsFile (human-readable XML, one file per owner). To plug a different backend, implement IComponentSettingsProvider and register it under a key:

public interface IComponentSettingsProvider
{
    TSettings Get<TSettings>(SettingsNodePath path) where TSettings : class, new();
    void Set<TSettings>(SettingsNodePath path, TSettings value) where TSettings : class, new();
}

services.AddComposableSettingsFile("gui", new MyJsonStore(path));

Node paths address a model within a file: SettingsNodePath.Root("gui").Child("clock") (gui/clock). Valid segment characters: [A-Za-z0-9_.-]+.

Source generators

Generator Trigger Emits
SettingsModelGenerator [SettingsModel] partial class INotifyPropertyChanged, properties (scalar / collection / nested), and a constructor that tracks nested members
SettingsConsumerGenerator [SettingsConsumer(typeof(T))] partial class Settings pass-through property + InitializeGeneratedSettings(provider) + INPC relay
ObservableSettingsGenerator [SettingsVm(typeof(T))] partial class (existing INPC) Settings pass-through + InitializeSettings(provider) + relay into OnPropertyChanged + [SettingsProxy] bodies + DisposeGeneratedSettings()

Diagnostics

Id Meaning
CSP020 [SettingsModel] class must be partial
CSP021 [SettingsModel] class must not already implement INotifyPropertyChanged
CSP022 [SettingsConsumer] class must be partial
CSP023 [SettingsConsumer] requires a settings type argument
CSP024 [SettingsConsumer] class must not already implement INotifyPropertyChanged
CSP025 [SettingsModel] with tracked members must not declare a constructor (the generator owns it)
CSP026 a nested-object field must be non-readonly (the generator owns its setter; readonly is fine for collections)
CSP030 [SettingsVm] class must be partial
CSP031 [SettingsVm] requires a settings type argument
CSP032 [SettingsProxy] has no matching settings model member
CSP033 [SettingsProxy] type does not match the settings member type

Conventions & limitations

  • Observable lists are ObservableCollection<T>. Collection item types should be [SettingsModel] if you want in-place item edits to persist; value-like items (string/primitive/enum) persist on add/remove/replace.
  • Nested-object fields must be non-readonly.
  • Dictionary<,> is not supported.
  • Settings models are POCOs with [A-Za-z0-9_.-]+ node names; nested classes as the model itself are not supported.

Examples

See examples/:

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
1.0.13 93 6/1/2026
1.0.12 87 5/26/2026
1.0.11 100 5/26/2026
1.0.10 89 5/26/2026
1.0.9 89 5/26/2026
1.0.8 80 5/26/2026
1.0.7 90 5/25/2026
1.0.3 93 5/19/2026
1.0.1 90 5/17/2026
1.0.0 99 5/17/2026
1.0.0-preview.2 48 5/17/2026