Modulation API
Every optional behavior in the library — encryption, Base64, versioning, recovery and autosave — is a module. Modules attach to a per-object socket and subscribe to events on the load/save pipeline.
Key points
- All modules are stored inside
JsonSettings.Modulation(aModuleSocket). - The
ModuleSocketstores every module attached to thatJsonSettingsobject. - Every settings object gets its own module instances; nothing is shared between objects.
- Attaching modules is done via the fluent static extensions in
FluentJsonSettings(WithModule,WithEncryption,WithBase64,WithVersioning,WithRecovery). - All modules provided by the library have properties and methods suited for inheritance, so extending is easy.
// Attach a library module by type, or your own instance:
JsonSettings.Configure<MySettings>("config.json")
.WithModule<MySettings, MyModule>(/* constructor args */)
.LoadNow();
JsonSettings.Configure<MySettings>("config.json")
.WithModule(new MyModule())
.LoadNow();
Execution order
The events are intentionally many, to allow as much interception as possible. Handlers do not return data; instead they receive a reference to the object that can be modified and is then used in the next stage.
Loading
event BeforeLoadHandler BeforeLoad(JsonSettings sender, ref string source); // 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);
event AfterRepopulateHandler AfterRepopulate(JsonSettings sender, bool successfulPopulate);
event AfterDeserializeHandler AfterDeserialize(JsonSettings sender);
event AfterLoadHandler AfterLoad(JsonSettings sender);
BeforeRepopulate/AfterRepopulate bracket the JSON populate itself and are the only
per-populate signal: they also fire for LoadDefault(), versioning/recovery reloads and direct
LoadJson() calls, where the rest of the loading events do not. AfterRepopulate fires from a
finally and reports successfulPopulate: false when the populate threw halfway (the recovery
path), in which case the object may hold a mix of old and file values — check the flag before
acting on loaded data. Do not call Save() between the two events: the instance is mid-rewrite.
The library itself rides this pair — the autosave module suppresses save-on-write during the
populate through it, and the NotificationBinder rebinds replaced collections after it.
And, in the case of a JsonException during LoadJson:
// recovered marks whether recovery succeeded; handled prevents further modules from attempting to recover.
// If recovered is returned false, a JsonSettingsException is thrown with the original exception as inner.
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 ordering
When a handler attaches to Encrypt, it is pushed to the end of the event queue — it
receives the data after everything attached before it. When a handler attaches to Decrypt, it is
pushed to the beginning of the queue. This way encryption/encoding and decryption/decoding
automatically run in the correct, mirrored order, so you can stack (for example)
WithBase64().WithEncryption(...) and have decode/decrypt unwind correctly.
Writing your own module
Inherit Module, override Attach/Deattach, and subscribe/unsubscribe to the pipeline events. The
library's own Base64Module is a complete, minimal example:
using System;
using Nucs.JsonSettings.Modulation;
/// <summary>Will convert text to base64, not pure json.</summary>
public class Base64Module : Module {
public override void Attach(JsonSettings socket) {
base.Attach(socket);
socket.Encrypt += _Encrypt;
socket.Decrypt += _Decrypt;
}
public override void Deattach(JsonSettings socket) {
base.Deattach(socket);
socket.Encrypt -= _Encrypt;
socket.Decrypt -= _Decrypt;
}
protected virtual void _Encrypt(JsonSettings sender, ref byte[] data) {
data = JsonSettings.Encoding.GetBytes(Convert.ToBase64String(data));
}
protected virtual void _Decrypt(JsonSettings sender, ref byte[] data) {
data = Convert.FromBase64String(JsonSettings.Encoding.GetString(data));
}
}
Attach it like any other module:
var settings = JsonSettings.Configure<MySettings>("config.json")
.WithModule(new Base64Module())
.LoadNow();
Note
Module.Deattach is called automatically on Dispose, and the socket is held through a
WeakReference<JsonSettings> so a module never keeps a settings object alive.