-
Notifications
You must be signed in to change notification settings - Fork 1
09. SpecialTypes
SerializableObject{T}
is a wrapper around a reference or value type, that will serialize the inner value to a file, monitor the file to synchronize external changes, and notify of changes via an OnChanged
event.
The simplest use-case of this is for example you create a record
for your app settings, which then enables each setting to be type safe and specific. Then when you change it from code.
A JsonSerializerContext
for the type is required, it will makes it more performant and AOT compatible.
new SerializableObject(string path, T defaultValue, JsonSerializerContext jsonSerializerContext);
new SerializableObject(string path, JsonSerializerContext jsonSerializerContext); // uses the other constructor with the default{T}
The constructor first validates the path, if the directory doesn't exist or filename is empty, it will throw a IOException
, if the file doesn't exist, or the contents of the file are empty, it will serialize the default value to the file, otherwise it will deserialize from the file or set to the default if it fails.
In case you never created a JsonSerializerContext
, this is how:
imagine for the example that the object type is Configuration
// This needs to be under the namespace, it cannot be a nested class.
[JsonSourceGenerationOptions(WriteIndented = true)] // Optional
[JsonSerializable(typeof(Configuration))]
internal partial class JsonContext : JsonSerializerContext { }
// The source generator will take care of everything.
// Now an example of creating the object
public static readonly SerializableObject<Configuration> Config = new(_path, JsonContext.Default);
// Notice how we passed the JsonContext
void Modify(Func<T, T> modifier)
Modification is done using a function, this is to both enable an experience similar to options
and to make it work with struct
s because they are value types.
Modify(person => {
person.Name = "New";
return person;
}); // Simple change that will work with reference types or value types
// If person was a record, it is even easier
Modify(person => person with { Name = "New" });
The event that notifies for changes is OnChanged
, and you need to subscribe to it with a signature of void Function(object sender, SerializedObjectEventArgs e)
, this is a special event args implementation that will contain the new value after the change. an anonymous function with the same parameters is also accepted.
For example:
var serializedObj = new SerializableObject(path, new Person { Name = "Dave" });
monitoredObj.OnChanged += OnValueChanged
private void OnValueChanged(object sender, SerializedObjectEventArgs e) {
Console.WriteLine($"The new name is {e.Value.Name}");
}
This basically concludes the general usage.
MonitoredSerializableObject{T}
is an extension of SerializableObject{T}
which adds functionality of watching the filesystem to synchronize external changes, usage is basically identical except MonitoredSerializableObject{T}
also implements IDisposable
to release the resources of the file system watcher.
- In order to avoid file writing exceptions,
Modify
is synchronized using a lock, to only be performed by a single thread at a time. - There is also an internal mechanism that should prevent deserialization after internal modification in order to reduce io operations and redundant value updates.
- Both variants of
SerializableObject{T}
implementIDisposable
and should be disposed of properly, but their main use-case is to be initialized once and used throughout the lifetime of the application, so this isn't absolutely crucial, and they both implement a finalizer that will dispose of the resources anyway.