Brave Squire .net tools

Visit the itch.io page to get the tools.

  • Update: Safe state is now called BraveSquire.BinarySerialization and published together with other small tools I made. I update the package occasionally as it is, so there may always be things in there which work not so well or even not at all.
  • Blog post on some of the updates  in V1.4.27.122

Safe State is a binary serialization system for use in .net (including unity3d) applications. It has been created because I do not seem to be able to stop accidentally making frameworks although all I need is a simple subsystem.

  • Offers full control over serialization
  • Produces small output – quickly
  • Handles circular dependencies smoothly
  • Can perform dynamic typing out of the box
  • Can handle changes in serialized types
  • Fits nicely into a dependency injection environment (not a must though).

If you really, really want to know about its inner workings, you can read about the making of Safe State here.

And if you have suggestions or questions or want to say hi, you can go to the community page of our itch.io page here.

Quick start guide

Some properties of Safe State

  • Awfully interconnected graphs of objects are fluently handled. When deserialized, references will point where they should. No ugly object clones
  • Objects of reference types are stored only once and are referenced as a 4 byte integer afterwards.
  • A 4 byte integer uses no more than 4 bytes of serialized data
  • Dynamic type data (16 byte Guids in Safe State) is stored only once and is referenced as a 2 byte integer afterwards. It’s completely omitted if you tell the serializer to do so.
  • (De)Serialization of a type is fully controllable including data based control flow and such nice things
  • Because type information is stored as Guids, you can rename types as you please
  • Because you have full control over the (de)serialization process, you could even replace types or completely change your data structures and still use data serialized earlier if you implemented a clever custom type serializer
  • The one downside of full control is that you have to take it. So you can’t simply pass an object to Safe State and expect it to make bytes out of it. Use BinaryFormatter if that and only that is what you want. Instead, you have to write a little (seriously, it’s laughable in most cases) code to make a type serializable with the system.

Basic usage

Safe State has two interfaces for clients to use. One for serialization and one for deserialization.

In order to be able to use Safe State, you have to set up the system. I recommend using a dependency injection container. In the following example, we are using a singleton implementation for the clients to access the serialization system instead. While this is not a very clean way to integrate any system, it’s sufficient to show the setup process. It’s entirely up to you, how you integrate the system into your application.

Type serializers

Update: From V1.4.27.122 onwards, there’s no need to implement a type serializer for more simple scenarios. See here for more information.

In order to make a type serializable for Safe State, you implement a type serializer for that type and pass it to the type serializer repository as seen above. The abstract type BaseTypeSerializer aids you in doing so.

  • The constructor has two parameters:
    • byValue is true if the type is treated like a value type (i.e. no references are stored).
    • typeCodeRepresentation is a string representing a guid identifying the type. In Visual Studio, use “Tools/Create GUID” and pick “Registry format” to create one.
  • The constructor parameters of a type serializer should never ever change. Otherwise, serialized data written with older versions becomes unreadable.
  • WriteCreationDataTyped writes data which is read in CreateTyped
  • WriteInitializationDataTyped writes data which is read in InitializeTyped
  • CreateTyped creates an instance using data written in WriteCreationDataTyped
  • InitializeTyped initializes an instance using data written in WriteInitializationDataTyped

Serializer targets or deserializer sources are passed to the serialization methods of a type serializer.

  • WriteByte / ReadByte read or write a single byte
  • WriteData / ReadData read or write successive data (value types, structs)
  • Write<T> / Read<T> read or write values or objects of any type
  • WriteDynamic<T> / ReadDynamic<T> read or write objects of any type. The type is determined at runtime.
  • WriteArray<T> / ReadArray<T> read or write a set of objects or values
  • WriteDynamicArray<T> / ReadDynamicArray<T> read or write a set of objects where the type of each object is determined at runtime

Simple type serializer using ReadData / WriteData

IntSerializer reads or writes 4 bytes of successive data to store a System.Int32.

Simple type serializer for a value type

DateTimeSerializer serializes a System.DateTime.
As you can see, you can write and read any value or object as long as there’s a type serializer handling its type.
As a rule of thumb, you do not have to implement WriteInitializationDataTyped and InitializeTyped if (only if) byValue is set to true in the constructor.

Simple type serializer for a reference type

In order to have access to private members, you can implement your type serializers as inner classes of the type serialized. You can of course always implement a not nested type serializer but it’s not that straightforward anymore.

The downside of implementing nested type serializers is that serialization leaks into the actual implementation. You must decide for yourself if you care. I prefer to use nested serializers because that way, only fields have to be read and set. You don’t have to call methods or set properties which could have side effects.
_favouriteBar is written and read dynamically because Bar is a type of which there are types derived from it.

Note: When dealing with reference types (byValue == false), you should read and write as much data as possible in WriteInitializationDataTyped and InitializeTyped.

Versioning in type serializers

Update: Since the last update of BraveSquire.Tools, There is a base type called BaseTypeSerializerVersioned<T, TVersion> which automatically handles versioning.

If you want to use Safe State for persistence and you are not over 9000% sure that a type is not going to change, you should use versioning right from the start.

InitializeTyped now handles deserialization according to the version read.
If version is less than 3 (i.e. _id has been written during serialization), _id is read but not used in order to advance the deserialization source to the next value.
If version is less than 2(i.e. _name has NOT been serialized during serialization), _name is initialized with a standard value.
Versioning is a good example for how you can be creative about how to serialize a type. It’s not a feature implemented into the system but rather it’s just some magic the type serializer does.

Dependency Injection and further initialization in type serializers

Some objects depend on other objects which are not part of the serialized data or need to be initialized further in order to work properly. For example, Foo could be dependent on a service which has to be injected after initialization and have an initialization method.

In order to be able to inject the service, the type serializer itself becomes dependent on that service.

Type serializers for derived types

Update: From V1.4.27.122 onwards, there’s a more straightforward way to handle derived types. See here for more information.

In order to easily implement a type serializer for a derived type, you can inject the base type’s serializer into the derived type’s serializer and use it there.

Type serializers for generic types

In order to be able to handle generic types, you implement a type serializer factory. Don’t worry, it’s easy. In fact, also BaseTypeSerializer<T> is a simple type serializer factory returning just one type serializer (itself). In order to implement a type serializer factory, you implement the abstract types TypeSerializerFactoryOneParameter (for one generic parameter) and BaseTypeSerializerFabricated<T>. The latter is a base type of BaseTypeSerializer<T> not implementing ITypeSerializerFactory.

HashSetSerializerFactory is a good example of a type serializer factory. As you can see, the type code is now passed to the constructor of the factory. You also pass the generic type definition of the handled type to it.
CreateSerializer<TInner> just returns the proper type serializer. The implementation of the generic type serializer itself is not very different from the implementation of a basic type serializer.