Nucs.JsonSettings.Autosave
An extension to JsonSettings that saves automatically when a property changes. Mark your settings class [Autosave] and every setter commits a save - no proxy, no virtual requirement, and nothing generated at runtime, so it works under Native AOT.
Install
| Product | Versions |
|---|---|
| .NET | |
| .NET Standard | |
| Other |
README
JsonSettings
This library aims to simplify the process of creating configuration for your C# app/service
by utilizing the serialization capabilities of Json.NET
to serialize nested (custom) objects, dictionaries and lists as simply as by creating a POCO and inheriting JsonSettings class.
📖 Full documentation & API reference: nucs.github.io/JsonSettings
Installation
dotnet add package Nucs.JsonSettings
dotnet add package Nucs.JsonSettings.Autosave # optional, for [AutoSave] and EnableAutosave()
dotnet add package Nucs.JsonSettings.NotifyChanges # optional, for [NotifyChanges] data binding
All packages are signed and supported runtimes are: netstandard2.0, net48, net6.0, net8.0 and net10.0.
Table of Contents
- 📖 Documentation Website
- Features Overview
- The Basics
- Modules
- Dynamic Settings Bag
- Changing JsonSerializerSettings
- Converters
- Modulation Api
- Native AOT and Trimming
- License
Features Overview
- Initialized in a fluent static API read more
- Cross-platform, multi-targeting
netstandard2.0,net48,net6.0,net8.0andnet10.0 - Modularity allowing easy extension and high control over behavior on a per-object level read more
- Autosaving on changes read more
- Versioning control read more
- Offers protection mechanisms such as renaming file and loading default
- By changing version, it allows to introduce any kind of changes to the settings class
- Customizable control over recovering from parsing exceptions read more
- AES256 Encryption via a key read more
- Fully extensible with Json.NET 's capabilities, attributes and settings
- It'll be accurate to say that this library is built around Json.NET
SettingsBag, adynamicoption that uses a ConcurrentDictionary<string,object> eliminating the need for hardcoding POCO class read more
The Basics
Test project: https://github.com/Nucs/JsonSettings/tree/master/tests/JsonSettings.Tests
Serialization Guide: https://www.newtonsoft.com/json/help/html/SerializationGuide.htm
JsonSettings is the base abstract class serving as the base class for all settings objects the user defines.
Creation, loading is done through static API where saving is through the settings object API.
Here is a self explanatory quicky of to how and what:
- Hardcoded settings
//Step 1: create a class and inherit JsonSettings
class MySettings : JsonSettings {
//Step 2: override a default FileName or keep it empty. Just make sure to specify it when calling Load!
//This is used for default saving and loading so you won't have to specify the filename/path every time.
//Putting just a filename without folder will put it inside the executing file's directory.
public override string FileName { get; set; } = "TheDefaultFilename.extension"; //for loading and saving.
#region Settings
public string SomeProperty { get; set; }
public Dictionary<string, object> Dictionary { get; set; } = new Dictionary<string, object>();
public int SomeNumberWithDefaultValue { get; set; } = 1;
[JsonIgnore] public char ImIgnoredAndIWontBeSavedOrLoaded { get; set; }
#endregion
//Step 3: Override parent's constructors
public MySettings() { }
public MySettings(string fileName) : base(fileName) { }
}
//Step 4: Load
public MySettings Settings = JsonSettings.Load<MySettings>("config.json"); //relative path to executing file.
//or create a new empty
public MySettings Settings = JsonSettings.Construct<MySettings>("config.json");
//Step 5: Introduce changes and save.
Settings.SomeProperty = "ok";
Settings.Save();
- Dynamic settings
- Dynamic settings will automatically create new keys.
- Can accept any Type that Json.NET can serialize
ValueTypes are returned asNullable<Type>, therefore if a key doesn't exist - a null is returned.
//Step 1: Just load it, it'll be created if doesn't exist.
public SettingsBag Settings = JsonSettings.Load<SettingsBag>("config.json");
//Step 2: use!
Settings["key"] = "dat value tho";
Settings["key2"] = 123;
dynamic dyn = Settings.AsDynamic();
if ((int?)dyn.key2==123)
Console.WriteLine("explode");
Settings.Save();
- Encrypted settings
- Uses AES via
System.Security.Cryptography(the .NET BCL); optional AES-GCM, AES-CCM, ChaCha20-Poly1305 or authenticated AES-CBC-HMAC. - Can be applied to any settings class because it is a module.
- The secret can be a text password, a binary password, or a raw key.
- Uses AES via
MySettings Settings = JsonSettings.Load<MySettings>("config.json", q=>q.WithEncryption("mysecretpassword"));
SettingsBag Settings = JsonSettings.Load<SettingsBag>("config.json", q=>q.WithEncryption("mysecretpassword"));
//or
MySettings Settings = JsonSettings.Configure<MySettings>("config.json")
.WithEncryption("mysecretpassword")
//or: .WithModule<EncryptionModule>("pass");
.LoadNow();
SettingsBag Settings = JsonSettings.Configure<SettingsBag>("config.json")
.WithEncryption("mysecretpassword")
//or: .WithModule<EncryptionModule>("pass");
.LoadNow();
The secret can also be supplied as bytes. A byte[] password is stretched into the key with
the same PBKDF2 derivation as a text password (salted and iterated); a raw key is used verbatim
and must be 16, 24 or 32 bytes (AES-128/192/256):
// binary password - PBKDF2-derived, like a text password but with arbitrary bytes.
// Note: NOT the same credential as the text password whose UTF-8 bytes equal these.
byte[] password = Encoding.UTF8.GetBytes("mysecretpassword");
var a = JsonSettings.Configure<MySettings>("config.json").WithEncryption(password).LoadNow();
// raw AES key - used as-is, no derivation. You own the key's quality.
byte[] key = RandomNumberGenerator.GetBytes(32); // or from an env var / HSM / another KDF
var b = JsonSettings.Configure<MySettings>("config.json").WithEncryptionRawKey(key).LoadNow();
// both accept a fetcher, incl. one that receives the instance:
var c = JsonSettings.Configure<MySettings>("config.json")
.WithEncryptionRawKey(() => LoadKeyFromVault())
.LoadNow();
- Hardcoded Settings with Autosave
- Automatic save will occur when any property changes
- Works on any property —
virtualis not required (as of 2.2.0); opt a property out with[IgnoreAutosave] - Requires package
nucs.JsonSettings.Autosaveand an[Autosave]attribute on the class.
Settings x = JsonSettings.Load<Settings>().EnableAutosave(); //call after loading
//or:
ISettings x = JsonSettings.Load<Settings>().EnableIAutosave<Settings, ISettings>(); //Settings implements interface ISettings
x.Property = "value"; //Saved!
- Dynamic Settings with Autosave
- Automatic save will occur when changes detected
- note: SettingsBag has it's own implementation of EnableAutosave().
//Step 1:
SettingsBag Settings = JsonSettings.Load<SettingsBag>("config.json").EnableAutosave(); //call after loading
//Step 2:
Settings.AsDynamic().key = "wow"; //Saved!
Settings["key"] = "wow two"; //Saved!
Recovery
RecoveryModule provides handling for JsonException when calling JsonSettings.LoadJson during the loading process.
On a scenario of exception/failure, one of the following actions can take place:
RecoveryAction.Throw
Will throw JsonSettingsRecoveryException with the real exception as inner exception.
RecoveryAction.LoadDefault
Default settings will be loaded without touching the existing file until next save.
RecoveryAction.LoadDefaultAndSave
Default settings will be loaded and saved to disk immediately.
RecoveryAction.RenameAndLoadDefault
Will append the version to the end of the faulty file's name and load the default settings and save to disk.
i.e.
myfile.jsonversioned1.0.0.5will be renamed tomyfile.1.0.0.5-0.jsonif it fails on parsing (the trailing-0is a collision counter — a second archive becomes-1, and so on) and the new default settings will be saved as the original filename.
All recovery properties and methods are suited for inheritance so extending is quite easy.
using Nucs.JsonSettings;
using Nucs.JsonSettings.Modulation.Recovery;
//attach RecoveryModule via the fluent extension and pick what happens on a parse failure:
var settings = JsonSettings.Configure<MySettings>("config.json")
.WithRecovery(RecoveryAction.RenameAndLoadDefault)
.LoadNow();
settings.SomeProperty = "hello";
settings.Save();
//...later config.json is corrupted on disk (hand-edited, truncated, a half-finished write).
//Loading again does NOT throw: the corrupt file is renamed aside and defaults are loaded.
settings = JsonSettings.Configure<MySettings>("config.json")
.WithRecovery(RecoveryAction.RenameAndLoadDefault)
.LoadNow();
//config.json is now the freshly-saved default; the corrupt copy is preserved next to it as
//config.<version>-0.json (or config.0.json when the class is not IVersionable).
Recovery composes with versioning: versioning runs when the file parses, recovery catches the parse itself failing.
Versioning
VersioningModule<T> provides the ability to enforce a specific version so when new changes are introduced to your Settings class (scheme),
a user-defined action can take place. Any of the following actions can be taken:
VersioningResultAction.DoNothing
Will keep the old version if it was parsed by Json.NET successfully. otherwise RecoveryModule will handle the failure of loading.
VersioningResultAction.Throw
Will throw JsonSettingsRecoveryException with the real exception as inner exception.
VersioningResultAction.LoadDefault
Default settings will be loaded without touching the existing file until next save.
VersioningResultAction.LoadDefaultAndSave
Default settings will be loaded and saved to disk immediately.
VersioningResultAction.RenameAndLoadDefault
Will append the version to the end of the faulty file's name and load the default settings and save to disk.
i.e.
myfile.jsonversioned1.0.0.5will be renamed tomyfile.1.0.0.5-0.jsonif it fails on parsing (the trailing-0is a collision counter — a second archive becomes-1, and so on) and the new default settings will be saved as the original filename.
There are two ways to specify which version to enforce.
Pass the version when calling
WithVersioning.Add
[EnforcedVersion("1.0.0.0")]attribute to yourIVersionable.Versionproperty definition.When dealing with inheritance/virtual override, the attribute of the lowest inherited class will be used.
using Nucs.JsonSettings;
using Nucs.JsonSettings.Modulation;
//The settings class must implement IVersionable (contributes `Version Version { get; set; }`).
class MySettings : JsonSettings, IVersionable {
public override string FileName { get; set; } = "config.json";
public virtual Version Version { get; set; } = new Version(1, 0, 0, 0);
public string Theme { get; set; } = "dark";
public MySettings() { }
public MySettings(string fileName) : base(fileName) { }
}
//1) Pass the enforced version explicitly:
var settings = JsonSettings.Configure<MySettings>("config.json")
.WithVersioning("1.0.0.0", VersioningResultAction.RenameAndLoadDefault)
.LoadNow();
//Later you ship a new scheme and bump the enforced version. A file still written as 1.0.0.0 no
//longer matches, so it is renamed to config.1.0.0.0-0.json and a fresh default config.json is saved.
settings = JsonSettings.Configure<MySettings>("config.json")
.WithVersioning("2.0.0.0", VersioningResultAction.RenameAndLoadDefault)
.LoadNow();
//2) ...or bake the version into the class with [EnforcedVersion] and use the version-less overload:
// [EnforcedVersion("2.0.0.0")]
// public virtual Version Version { get; set; } = new Version(1, 0, 0, 0);
var byAttribute = JsonSettings.Configure<MySettings>("config.json")
.WithVersioning(VersioningResultAction.RenameAndLoadDefault)
.LoadNow();
Policy
A comparison between versions is done by the Policy which is a VersioningPolicyHandler delegate ((Version, Version) => bool) passed during the construction of VersioningModule<T> or falls back to static VersioningModule<T>.DefaultPolicy which can be changed.
It is possible to change the static default policy by changing VersioningModule<T>.DefaultPolicy although each VersioningModule<T> can be assigned its own policy.
By default the versions must match exactly:
static bool DefaultEqualPolicy(Version version, Version expectedVersion) {
return expectedVersion?.Equals(version) != false;
}
Encryption
The default is AES-256-CBC over the serialized JSON (UTF-8 bytes), using only System.Security.Cryptography (the .NET base class library) — no third-party cryptography. The file holds a random IV followed by the AES-CBC ciphertext. Add WithBase64() to additionally store the result as copy-pasteable base64 text.
The secret comes in three forms:
| Call | Secret | How it becomes the key |
|---|---|---|
WithEncryption(string) / WithEncryption(SecureString) |
text password | PBKDF2 (salted, iterated) |
WithEncryption(byte[]) |
binary password | the same PBKDF2 derivation, over the raw bytes |
WithEncryptionRawKey(byte[]) |
raw AES key (16/24/32 bytes) | used verbatim, no derivation |
Each also has Func<...> and Func<T, ...> overloads for resolving the secret lazily (e.g. from a
vault or an environment variable).
Notes:
- A
byte[]password is a different credential from the text password whose UTF-8 encoding equals those bytes — the text derivation folds in the string's character length, which raw bytes do not carry. Pick one form per file. - A raw key skips PBKDF2, so its strength is entirely the key you provide; supply high-entropy
key material (e.g.
RandomNumberGenerator.GetBytes(32)), not a low-entropy value. - The on-disk format is identical across all three (a random IV followed by AES-CBC blocks), and the text-password path is byte-for-byte compatible with every earlier version.
// text password (classic)
JsonSettings.Configure<MySettings>("config.json").WithEncryption("mysecretpassword").LoadNow();
// binary password (PBKDF2-derived)
JsonSettings.Configure<MySettings>("config.json").WithEncryption(passwordBytes).LoadNow();
// raw key (verbatim, 16/24/32 bytes for AES)
JsonSettings.Configure<MySettings>("config.json").WithEncryptionRawKey(key32).LoadNow();
The default AesCbc is unauthenticated and on-disk compatible with every earlier version. Pass an
EncryptionAlgorithm to choose another — including authenticated algorithms that detect a
tampered file, not only keep it confidential:
// authenticated AEAD (.NET 6.0+)
JsonSettings.Configure<MySettings>("config.json").WithEncryption("password", EncryptionAlgorithm.AesGcm).LoadNow();
JsonSettings.Configure<MySettings>("config.json").WithEncryptionRawKey(key32, EncryptionAlgorithm.ChaCha20Poly1305).LoadNow();
AesCbc and AesCbcHmac are available on every target framework; AesGcm, AesCcm and
ChaCha20Poly1305 require .NET 6.0+. There is no algorithm marker in the file, so read it back with
the same algorithm it was written with; only AesCbc reads files from older versions. Encryption runs
entirely on System.Security.Cryptography — there is no third-party cryptographic dependency.
Autosave
Autosaving appends a save to the end of every property setter of a class marked [Autosave].
This happens at compile time, via IL weaving (AspectInjector),
in the assembly that declares the class. Nothing is generated at runtime.
[Autosave]
public class MySettings : JsonSettings {
public override string FileName { get; set; } = "config.json";
public string Name { get; set; } // no 'virtual' required
public int Count { get; set; }
}
var settings = JsonSettings.Load<MySettings>("config.json").EnableAutosave();
settings.Name = "changed"; // saved
What changed in 2.2.0
Autosave used to build a runtime proxy with Castle.Core, which forced three restrictions
that are now gone:
| Before (Castle.DynamicProxy) | Now (compile-time weaving) |
|---|---|
Every public property had to be virtual |
Ordinary properties work; virtual is irrelevant |
The class could not be sealed |
sealed classes work |
EnableAutosave() returned a different object, so a reference captured beforehand silently did not autosave |
Returns the same instance; every reference to it autosaves |
Impossible under Native AOT (System.Reflection.Emit) |
No runtime codegen at all |
In exchange there is one new requirement: the class must carry [Autosave]. Calling
EnableAutosave() on a class without it throws JsonSettingsException rather than
silently doing nothing.
[Autosave] is not inherited. A setter is woven where it is declared, so every class in
a settings hierarchy that declares properties you want saved needs its own attribute.
Two smaller behavioural notes for anyone migrating from 2.1.0:
virtualis no longer an opt-out. Under the proxy, a non-virtual property was silently skipped; some code relied on that to keep a property out of autosaving. Every setter is now woven regardless ofvirtual, so a property that must not autosave has to say so with[IgnoreAutosave](or[JsonIgnore]).EnableAutosave()is idempotent. Calling it twice on the same instance returns that instance and does not attach a second autosave module.
The Castle-era JsonSettingsAutosaveExtensions.Options field (a Castle.DynamicProxy.ProxyGenerationOptions)
is removed, since the type it exposed no longer exists in the dependency graph.
Attributes
Properties can be marked with IgnoreAutosaveAttribute (JsonIgnoreAttribute will also work)
to be excluded from the monitored properties for changes. This applies to collections too: an
[IgnoreAutosave] ObservableCollection does not save when its contents change.
Behaviour notes
- Indexers are not monitored. Writing
settings[key] = valuedoes not autosave — an indexer is not a serializable property. Use a normal property or callSave(). - Reentrancy is safe. Writing a monitored property from inside an
AfterSavehandler does not trigger another save (it would otherwise recurse); the value is kept in memory and persists on the next save. SuspendAutosavenests. Nested suspension scopes are reference-counted and collapse into a single save when the outermost scope closes; an inner scope closing does not end suspension.- A failing save surfaces at the assignment. If the triggered
Save()throws, the exception propagates out of the property assignment (the new value is already set in memory). - Disposing the settings unbinds autosave, including handlers attached to nested collections.
- Loading does not autosave.
Load(),LoadDefault()and a versioning reload populate the object from disk through its setters; those writes are not user edits and do not save back (autosave resumes normally afterward). IVersionable.Versionis not monitored. It is framework metadata managed by the versioning module and rides along in every ordinary save, so changing it does not by itself autosave. (A property namedVersionon a class that does not implementIVersionableis ordinary user data and is monitored.)
Requirements
- Install
nucs.JsonSettings.Autosavenuget package - Mark the settings class
[Autosave] - Call
mySettings.EnableAutosave()extension after callingLoad
How the weave runs (out of process, since 2.3.0)
AspectInjector's stock in-process MSBuild task leaks file handles into the MSBuild node, which
deterministically failed small executable consumers at the SDK's CreateAppHost step
(MSB4018 / "The process cannot access the file '<App>.dll' because it is being used by
another process") — merely referencing the package was enough. Since 2.3.0 the shipped build
targets run the identical weaver task in a short-lived child MSBuild process instead, so every
leaked handle is closed at child exit before CreateAppHost runs; weaving behaviour and
incrementality are unchanged. Opt back into the in-process weave with
<NucsJsonSettingsOutOfProcWeave>false</NucsJsonSettingsOutOfProcWeave>. See
docs/aspectinjector-2.9.0-apphost-lock.md for the full forensics.
Strong-named consumers
IL weaving rewrites the assembly after the compiler has signed it, and AspectInjector 2.9.0
retired its re-signing feature.
The package therefore ships MSBuild targets that re-sign the assembly with your own
$(AssemblyOriginatorKeyFile) after the weave. If sn.exe cannot be found the build warns
(NJS1001) rather than failing; opt out entirely with
<NucsAutosaveResignAfterWeaving>false</NucsAutosaveResignAfterWeaving>.
Suspend Autosave
In some scenarios, there might be multiple close changes to the configuration object. Normally that would trigger multiple save calls.
To prevent that, the developer can create a SuspendAutosave object which will postpone the save to when SuspendAutosave will be disposed or Resume called.
If there were no changes between the allocation of SuspendAutosave object and disposal/resume then save won't be called.
var settings = JsonSettings.Load<MySettings>("config.json").EnableAutosave();
using (settings.SuspendAutosave()) {
settings.Width = 800; // does not save yet
settings.Height = 600; // does not save yet
settings.Title = "App"; // does not save yet
} // one save here on dispose — and only if something changed
//or drive it manually instead of a using-block:
var suspender = settings.SuspendAutosave();
settings.Width = 1024;
suspender.Resume(); // commits the single pending save (same as Dispose); a second call is a no-op
SuspendAutosave() resolves the object's suspension module — the AutosaveModule on a woven
class, the bag's own SettingsBagAutosaveModule on a SettingsBag — so call EnableAutosave() first. Scopes
are reference-counted and nest — only the outermost one commits, once.
WPF Support with INotifyPropertyChanged/INotifyCollectionChanged
Any settings class can turn into a ViewModel with full autosave support making window settings and state persistence much simpler.
When your settings class inherits INotifyPropertyChanged, upon calling EnableAutosave,
a NotificationBinder is attached to the settings object that'll listen to the settings class's:
event PropertyChangedcalls- All properties that implement
INotifyPropertyChangedwill bind to theirevent PropertyChanged - All properties that implement
INotifyCollectionChangedsuch asObservableCollection<T>will bind to theirevent CollectionChanged - All other properties save through their woven setter (
virtualis not required as of 2.2.0).
So evidently, objects inside ObservableCollection or other nested properties that are not in the settings class are not monitored for changes.
Saving on a plain property write is handled by the woven setter, so a hand-written setter that
raises OnPropertyChanged and an auto-implemented one behave identically. The
NotificationBinder is what re-binds nested INotifyPropertyChanged /
INotifyCollectionChanged objects when the property holding them is replaced.
Requirements
- Settings class inherit
INotifyPropertyChanged(e.g. by derivingNotifiyingJsonSettings) - Mark the settings class
[Autosave] - Install
nucs.JsonSettings.Autosavenuget package - Call
mySettings.EnableAutosave()extension after callingLoad
Producing notifications for the View — [NotifyChanges]
The above makes autosave react to PropertyChanged. To make a setter raise it — so a binding
(WPF, WinForms, Avalonia, WinUI, MAUI, Uno) refreshes — without hand-writing OnPropertyChanged() in
every setter, including on auto-properties (which otherwise save but never notify), install the
separate Nucs.JsonSettings.NotifyChanges package and mark the class [NotifyChanges]:
[Autosave, NotifyChanges] // [Autosave] from Nucs.JsonSettings.Autosave,
public class WindowSettings : NotifiyingJsonSettings { // [NotifyChanges] from Nucs.JsonSettings.NotifyChanges
public override string FileName { get; set; } = "window.json";
public double Width { get; set; } // binds two-way, saves, and notifies — no boilerplate
public string Title { get; set; }
}
- Compile-time weave like
[Autosave], not inherited, and composes with it (one write saves and notifies once). Put it on auto-properties — a hand-written setter that already callsOnPropertyChanged()would notify twice. Framework-neutral: depends only onSystem.ComponentModel, not on WPF. NotificationGuardcontrols when it fires, per class or per property:OnlyChanged(default),SkipNullOrDefault,Always— and they combine ([Flags]).- Silence a property with
[IgnoreNotify](independent of[IgnoreAutosave]— a property can save without notifying, or the reverse); frameworkFileName/Modulation/Versionnever notify. - The class must own the event:
NotifiyingJsonSettings, or an MVVM base recognised by convention (OnPropertyChanged/RaisePropertyChanged/NotifyOfPropertyChange). For a class with no base,[NotifyChangesMixin]injectsINotifyPropertyChangedfor you (per-instance; best for a single class — a hierarchy should useNotifiyingJsonSettings+[NotifyChanges]). - Also raises
INotifyPropertyChangingbefore the change (onNotifiyingJsonSettings, a convention raiser, or the mixin), fans a change out to a computed property with[NotifyChangesFor(nameof(…))], and marshals notifications onto the UI thread for off-thread writes viaEnableNotificationMarshaling().
See the Notifications & Data Binding guide for the guard
details, the mixin, INotifyPropertyChanging, [NotifyChangesFor], SynchronizationContext
marshalling, nested-collection autosave, threading, and a comparison with Fody PropertyChanged,
CommunityToolkit.Mvvm and ReactiveUI.
For a runnable tour, examples/JsonSettings.Examples.UI is a
WPF app in which every control is a bound settings property — the window's own position/size/title
persist through the binding, and one tab per integration (guards, [NotifyChangesFor], the
opt-outs, nested collections, the mixin, raiser conventions, EnableIAutosave, marshalling) shows
its save/notification counters, an activity log and the JSON file on disk, live:
dotnet run --project examples/JsonSettings.Examples.UI -f net8.0-windows
Throttled Save
Upcoming feature...
Dynamic Settings Bag
SettingsBag internally stores a key-value dictionary. Any type of Value can be passed as long as Json.NET knows how to serialize it.
SettingsBag has built-in feature for autosaving that can be enabled by calling EnableAutosave without WPF binding support.
var bag = JsonSettings.Load<SettingsBag>("bag.json").EnableAutosave();
bag["Name"] = "value"; // saved
bag.Remove("Name"); // saved
dynamic d = bag.AsDynamic();
d.Other = 42; // saved (routes through the bag)
This is a separate autosave from the [Autosave] weaving used for typed classes — it is
dictionary-backed, needs no attribute, and is what SettingsBag.EnableAutosave() (the instance
method) turns on. Its own SettingsBagAutosaveModule shares the SuspensionModule state
machine with the woven path, so it inherits the same guarantees:
SuspendAutosave() (including nesting), reentrancy safety (writing the bag inside an AfterSave
handler does not recurse), and Remove/RemoveWhere autosave like an index write.
Notes:
- Calling the
EnableAutosave()extension on aJsonSettings-typed reference to a bag routes to the bag's own autosave, so it behaves the same as calling the instance method. AsDynamic()returns a disposable wrapper; using it afterDispose()throwsObjectDisposedException.
Changing JsonSerializerSettings
The default settings are defined on static JsonSettings.SerializationSettings.
public static JsonSerializerSettings SerializationSettings { get; set; } = new JsonSerializerSettings {
Formatting = Formatting.Indented,
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
NullValueHandling = NullValueHandling.Include,
ContractResolver = new FileNameIgnoreResolver(),
TypeNameHandling = TypeNameHandling.Auto,
MaxDepth = 128
};
To alter the JsonSerializerSettings, it's best to understand how the library is resolving which settings to use during serialization/deserialization as follows:
/// <summary>
/// Returns configuration based on the following fallback: <br/>
/// settings ?? this.OverrideSerializerSettings ?? JsonSettings.SerializationSettings ?? JsonConvert.DefaultSettings?.Invoke()
/// ?? throw new JsonSerializationException("Unable to resolve JsonSerializerSettings to serialize this JsonSettings");
/// </summary>
/// <param name="settings">If passed a non-null, This is the settings intended to use, not any of the fallbacks.</param>
/// <exception cref="JsonSerializationException">When no configuration valid was found.</exception>
protected virtual JsonSerializerSettings ResolveConfiguration(JsonSerializerSettings? settings = null) {
return settings
?? this.OverrideSerializerSettings
?? JsonSettings.SerializationSettings
?? JsonConvert.DefaultSettings?.Invoke()
?? throw new JsonSerializationException("Unable to resolve JsonSerializerSettings to serialize this JsonSettings");
}
settingsparameter is an internal mechanism when handling defaults. If passed a non-null, This is the settings intended to use, not any of the following fallbacks.this.OverrideSerializerSettingsis a property in every class inheritingJsonSettingsallowing personalized settings per object. TheOverrideSerializerSettingsproperty andResolveConfigurationmethod are bothvirtualand can be overriden to redirect the resolving to where ever you see fit or with what-ever predefined value.static JsonSettings.SerializationSettingsis the default for allJsonSettingsobjects.static JsonConvert.DefaultSettingsis the default settings defined on a Json.NET level.
Converters
Defining converters or changing the serialization settings globally can be done by adding a converter to static JsonSettings.SerializationSettings as follows:
//call during app startup
JsonSettings.SerializationSettings.Converters.Add(new Newtonsoft.Json.Converters.VersionConverter());
Alternatively per object setting can be done by setting or inheriting JsonSettings.OverrideSerializerSettings property but
it is important to also specify the default configuration so JsonSettings behavior will remain persistent (see more) .
JsonConverterAttribute
By far the easiest way to specify a converter is by specifying a JsonConverterAttribute on the property and Json.NET will do the rest.
[JsonConverter(typeof(ExchangeConverter))]
public ExchangeType Exchange { get; set; }
JsonConverterAttribute can also be specified on an interface property as it is used in IVersionable and will apply to any class inheriting it.
This is the best approach for other libraries because by specifying an attribute, no matter what JsonSerializerSettings will be specified by the developer, Json.NET will always serialize this property with the specified converter.
public interface IVersionable {
[JsonConverter(typeof(Newtonsoft.Json.Converters.VersionConverter))]
public Version Version { get; set; }
}
Modulation Api
Key points
- All modules are stored inside
JsonSettings.ModuleSocket Modulation { get; }. ModuleSocketstores all modules attached to thisJsonSettingsobject.- Every settings object gets a new module object allocated for every module configured.
- Attaching modules is done via static extensions read more
- All modules provided by the library have properties and methods that are suited for inheritance so extending is easy.
using Nucs.JsonSettings;
using Nucs.JsonSettings.Modulation;
using Nucs.JsonSettings.Modulation.Recovery;
//Attach a module fluently — by instance, or by type with constructor arguments:
var settings = JsonSettings.Configure<MySettings>("config.json")
.WithModule(new Base64Module()) // your own instance
.WithModule<MySettings, RecoveryModule>(RecoveryAction.LoadDefault) // constructed for you
.LoadNow();
WithEncryption, WithBase64, WithVersioning and WithRecovery are all thin wrappers over
WithModule — e.g. WithRecovery(action) is exactly WithModule(new RecoveryModule(action)).
With Construct. Construct<T>(args) is the constructor-args sibling of Configure<T>(filename):
both hand back a fresh, fully-configured instance that has not read the file yet, so you can wire
up modules (or seed defaults in memory) before an explicit Load/Save:
//build a fresh instance, attach modules, then load explicitly:
var settings = JsonSettings.Construct<MySettings>("config.json") // ctor args go to your constructor
.WithModule(new Base64Module())
.WithEncryption("password")
.LoadNow();
//or use it purely in-memory — seed defaults and write them without reading an existing file:
var seeded = JsonSettings.Construct<MySettings>("config.json");
seeded.SomeProperty = "default";
seeded.Save();
Execution Order
The events are many to allow as much interception as possible.
The event handlers do not return any data but instead they receive a reference of the object that can be modified and will be used in the next stage.
Loading
event BeforeLoadHandler BeforeLoad(JsonSettings sender, ref string source); //source is the file that will be loaded.
event DecryptHandler Decrypt(JsonSettings sender, ref byte[] data);
event AfterDecryptHandler AfterDecrypt(JsonSettings sender, ref byte[] data);
event BeforeDeserializeHandler BeforeDeserialize(JsonSettings sender, ref string data);
event BeforeRepopulateHandler BeforeRepopulate(JsonSettings sender); //brackets the populate itself; fires on EVERY populate incl. LoadDefault and direct LoadJson
event AfterRepopulateHandler AfterRepopulate(JsonSettings sender, bool successfulPopulate); //from a finally; false when the populate threw halfway
event AfterDeserializeHandler AfterDeserialize(JsonSettings sender);
event AfterLoadHandler AfterLoad(JsonSettings sender, bool successfulLoad);
And in a case of JsonException during LoadJson
//recovered marks if a recovery from failure was successful, handled will prevent any further modules from attempting to recover.
//if recovered is returned false, JsonSettingsException will be thrown with the original exception as inner exception
event TryingRecoverHandler TryingRecover(JsonSettings sender, string fileName, JsonException? exception, ref bool recovered, ref bool handled);
event RecoveredHandler Recovered(JsonSettings sender);
Saving
event BeforeSaveHandler BeforeSave(JsonSettings sender, ref string destinition);
event BeforeSerializeHandler BeforeSerialize(JsonSettings sender);
event AfterSerializeHandler AfterSerialize(JsonSettings sender, ref string data);
event EncryptHandler Encrypt(JsonSettings sender, ref byte[] data);
event AfterEncryptHandler AfterEncrypt(JsonSettings sender, ref byte[] data);
event AfterSaveHandler AfterSave(JsonSettings sender, string destinition);
Cryptography / Encoding Decoding
When attaching to OnEncrypt event, it'll push to the end of the event queue - meaning it will receive the data after all the events/modules that were attached to it before.
When attaching to OnDecrypt, it is pushed to the beginning of the event queue.
Hence encryption/encoding and decryption/decoding is automatically in the right order.
Changes from Previous Version
Adoption Assessment
Assess this package against its supported frameworks, license, maintenance signals and dependency graph before making it a project standard.
Related Guides & Workflows
Learn how to evaluate, integrate, or audit this package in your .NET project.