Skip to content

09. SpecialTypes

David Shnayder edited this page Jun 3, 2024 · 4 revisions

Special Types

SerializableObject{T}

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.

Initialization

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

Modification

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 structs 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" });

Subscribing And Notifications

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}

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.

Notes

  • 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} implement IDisposable 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.