Awesome
Tinyhand
Tinyhand is a tiny and simple data format/serializer largely based on MessagePack for C# by neuecc, AArnott.
This document may be inaccurate. It would be greatly appreciated if anyone could make additions and corrections.
日本語ドキュメントはこちら
Table of Contents
Requirements
Visual Studio 2022 or later for Source Generator V2.
C# 12 or later for generated codes.
.NET 8 or later target framework.
Quick Start
Install Tinyhand using Package Manager Console.
Install-Package Tinyhand
This is a small sample code to use Tinyhand.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Tinyhand;
namespace ConsoleApp1;
[TinyhandObject] // Annote a [TinyhandObject] attribute.
public partial class MyClass // partial class is required for source generator.
{
// Key attributes take a serialization index (or string name)
// The values must be unique and versioning has to be considered as well.
[Key(0)]
public int Age { get; set; }
[Key(1)]
public string FirstName { get; set; } = string.Empty;
[Key(2)]
[DefaultValue("Doe")] // If there is no corresponding data, the default value is set.
public string LastName { get; set; } = string.Empty;
// All fields or properties that should not be serialized must be annotated with [IgnoreMember].
[IgnoreMember]
public string FullName { get { return FirstName + LastName; } }
[Key(3)]
public List<string> Friends { get; set; } = default!; // Non-null value will be set by TinyhandSerializer.
[Key(4)]
public int[]? Ids { get; set; } // Nullable value will be set null.
public MyClass()
{
// this.Reconstruct(TinyhandSerializerOptions.Standard); // optional: Call Reconstruct() to actually create instances of members.
}
}
[TinyhandObject]
public partial class EmptyClass
{
}
class Program
{
static void Main(string[] args)
{
// TinyhandModule_ConsoleApp1.Initialize(); // Initialize() method is required on some platforms (e.g Xamarin, Native AOT) which does not support ModuleInitializer attribute.
var myClass = new MyClass() { Age = 10, FirstName = "hoge", LastName = "huga", };
var b = TinyhandSerializer.Serialize(myClass);
var myClass2 = TinyhandSerializer.Deserialize<MyClass>(b);
b = TinyhandSerializer.Serialize(new EmptyClass()); // Empty data
var myClass3 = TinyhandSerializer.Deserialize<MyClass>(b); // Create an instance and set non-null values of the members.
var myClassRecon = TinyhandSerializer.Reconstruct<MyClass>(); // Create a new instance whose members have default values.
}
}
Performance
Simple benchmark with protobuf-net, MessagePack for C# and MemoryPack.
Tinyhand is quite fast and since it is based on Source Generator, it does not take time for dynamic code generation.
Method | Mean | Error | StdDev | Median | Gen0 | Allocated |
---|---|---|---|---|---|---|
SerializeProtoBuf | 401.90 ns | 1.847 ns | 9.089 ns | 397.35 ns | 0.0973 | 408 B |
SerializeMessagePack | 170.99 ns | 0.365 ns | 1.865 ns | 171.32 ns | 0.0134 | 56 B |
SerializeMemoryPack | 112.48 ns | 0.996 ns | 5.054 ns | 110.51 ns | 0.0229 | 96 B |
SerializeTinyhand | 80.51 ns | 0.104 ns | 0.524 ns | 80.72 ns | 0.0134 | 56 B |
DeserializeProtoBuf | 689.49 ns | 1.435 ns | 7.297 ns | 686.76 ns | 0.0763 | 320 B |
DeserializeMessagePack | 288.63 ns | 0.306 ns | 1.556 ns | 288.71 ns | 0.0668 | 280 B |
DeserializeMemoryPack | 124.30 ns | 0.367 ns | 1.895 ns | 123.35 ns | 0.0668 | 280 B |
DeserializeTinyhand | 145.02 ns | 1.230 ns | 6.186 ns | 149.17 ns | 0.0668 | 280 B |
SerializeMessagePackString | 178.74 ns | 0.286 ns | 1.446 ns | 178.45 ns | 0.0153 | 64 B |
SerializeTinyhandString | 128.12 ns | 0.196 ns | 0.986 ns | 127.69 ns | 0.0153 | 64 B |
SerializeTinyhandUtf8 | 650.83 ns | 0.720 ns | 3.589 ns | 650.99 ns | 0.0916 | 384 B |
SerializeJsonUtf8 | 495.27 ns | 1.119 ns | 5.672 ns | 495.46 ns | 0.0954 | 400 B |
DeserializeMessagePackString | 286.31 ns | 1.621 ns | 8.287 ns | 281.30 ns | 0.0668 | 280 B |
DeserializeTinyhandString | 175.70 ns | 0.531 ns | 2.624 ns | 175.77 ns | 0.0744 | 312 B |
DeserializeTinyhandUtf8 | 1,319.04 ns | 1.088 ns | 5.512 ns | 1,321.51 ns | 0.1526 | 640 B |
DeserializeJsonUtf8 | 1,045.53 ns | 1.286 ns | 6.574 ns | 1,047.47 ns | 0.2232 | 936 B |
Pitfalls
ModuleInitializer
Some AOT platforms (e.g Xamarin, Native AOT) currently does not support ModuleInitializer
attribute.
Tinyhand use ModuleInitializer
attribute to load generated formatters, so you need to call Initialize()
method manually on these platforms.
// Add this code before the first use of Tinyhand.
TinyhandModule_YourAssemblyName.Initialize(); // Assembly name is necessary to avoid name conflict in multiple assemblies.
Serialization Target
All public members are serialization targets by default. You need to add Key
attributes to public members unless ImplicitKeyAsName
is set to true.
[TinyhandObject]
public partial class DefaultBehaviourClass
{
[Key(0)]
public int X; // Key required
public int Y { get; private set; } // Not required since it's private setter.
[Key(1)]
private int Z; // By adding the Key attribute, You can add a private member to the serialization target.
}
[TinyhandObject(ImplicitKeyAsName = true)]
public partial class KeyAsNameClass
{
public int X; // Serialized with the key "X"
public int Y { get; private set; } // Not a serialization target (due to the private setter)
[Key("Z")]
private int Z; // Serialized with the key "Z"
[KeyAsName]
public int A; // Use the member name as the key "A".
}
Readonly and Getter-only
Readonly fields is not serialization target by default.
By explicitly adding a Key
attribute, you can make it a serialization target.
unsafe
compiler option is required to serialize readonly fields.
[TinyhandObject]
public partial class ReadonlyGetteronlyClass
{
[Key(0)]
public readonly int X; // `unsafe` required.
[Key(1)]
public int Y { get; } = 0; // Error!
}
Getter-only property is not supported.
Init-only property and Record type
Init-only property and record
type are supported.
[TinyhandObject]
public partial record RecordClass // Partial record required.
{// Default constructor is not required for record types.
[Key(0)]
public int X { get; init; }
[Key(1)]
public string A { get; init; } = default!;
}
[TinyhandObject(ImplicitKeyAsName = true)] // Short version, but string key is a bit slower than integer key.
public partial record RecordClass2(int X, string A);
Include private members
By setting IncludePrivateMembers
to true, you can add private and protected members to the serialization target.
[TinyhandObject(IncludePrivateMembers = true)]
public partial class IncludePrivateClass
{
[Key(0)]
public int X; // Key required
[Key(1)]
public int Y { get; private set; } // Key required
[IgnoreMember]
private int Z; // Add the IgnoreMember attribute to exclude from serialization targets.
}
Explicit key only
By setting ExplicitKeyOnly
to true, only members with the Key attribute will be serialized.
[TinyhandObject(ExplicitKeyOnly = true)]
public partial class ExplicitKeyClass
{
public int X; // Not serialized (no error message).
[Key(0)]
public int Y; // Serialized
}
Features
Handling nullable reference types
Tinyhand tries to handle nullable/non-nullable reference types properly.
[TinyhandObject(ImplicitKeyAsName = true)]
public partial class NullableTestClass
{
public int Int { get; set; } = default!; // 0
public int? NullableInt { get; set; } = default!; // null
public string String { get; set; } = default!;
// If this value is null, Tinyhand will automatically change the value to string.Empty.
public string? NullableString { get; set; } = default!;
// This is nullable type, so the value remains null.
public NullableSimpleClass SimpleClass { get; set; } = default!; // new SimpleClass()
public NullableSimpleClass? NullableSimpleClass { get; set; } = default!; // null
public NullableSimpleClass[] Array { get; set; } = default!; // new NullableSimpleClass[0]
public NullableSimpleClass[]? NullableArray { get; set; } = default!; // null
public NullableSimpleClass[] Array2 { get; set; } = new NullableSimpleClass[] { new NullableSimpleClass(), null! };
// null! will be change to a new instance.
public Queue<NullableSimpleClass> Queue { get; set; } = new(new NullableSimpleClass[] { null!, null!, });
// null! remains null because it loses information whether it is nullable or non-nullable in C# generic methods.
}
[TinyhandObject]
public partial class NullableSimpleClass
{
[Key(0)]
public double Double { get; set; }
}
public class NullableTest
{
public void Test()
{
var t = new NullableTestClass();
var t2 = TinyhandSerializer.Deserialize<NullableTestClass>(TinyhandSerializer.Serialize(t));
}
}
Default value
You can specify the default value for a member using DefaultValueAttribute
(System.ComponentModel).
If the serialized data does not have a matching data for a member, Tinyhand will set the default value for that member.
Primitive types (bool
, sbyte
, byte
, short
, ushort
, int
, uint
, long
, ulong
, float
, double
, decimal
, string
, char
, enum
) are supported.
[TinyhandObject(ImplicitKeyAsName = true)]
public partial class DefaultTestClass
{
[DefaultValue(true)]
public bool Bool { get; set; }
[DefaultValue(77)]
public int Int { get; set; }
[DefaultValue("test")]
public string String { get; set; }
[DefaultValue("Test")] // Default value for TinyhandObject is supported.
public DefaultTestClassName NameClass { get; set; }
}
[TinyhandObject(ImplicitKeyAsName = true)]
public partial class StringEmptyClass
{
}
[TinyhandObject]
public partial class DefaultTestClassName
{
public DefaultTestClassName()
{
}
public void SetDefault(string name)
{// To receive the default value, SetDefault() is required.
// Constructor -> SetDefault -> Deserialize or Reconstruct
this.Name = name;
}
public string Name { get; private set; }
}
public class DefaultTest
{
public void Test()
{
var t = new StringEmptyClass();
var t2 = TinyhandSerializer.Deserialize<DefaultTestClass>(TinyhandSerializer.Serialize(t));
}
}
You can skip serializing values if the value is identical to the default value, by using [TinyhandObject(SkipSerializingDefaultValue = true)]
.
Reconstruct
Tinyhand creates an instance of a member variable even if there is no matching data. By adding [Reconstruct(false)]
or [Reconstruct(true)]
to member attributes, you can change the behavior of whether an instance is created or not.
[TinyhandObject(ImplicitKeyAsName = true)]
public partial class ReconstructTestClass
{
[DefaultValue(12)]
public int Int { get; set; } // 12
public EmptyClass EmptyClass { get; set; } = default!; // new()
[Reconstruct(false)]
public EmptyClass EmptyClassOff { get; set; } = default!; // null
public EmptyClass? EmptyClass2 { get; set; } // null
[Reconstruct(true)]
public EmptyClass? EmptyClassOn { get; set; } // new()
/* Error. A class to be reconstructed must have a default constructor.
[IgnoreMember]
[Reconstruct(true)]
public ClassWithoutDefaultConstructor WithoutClass { get; set; }*/
[IgnoreMember]
[Reconstruct(true)]
public ClassWithDefaultConstructor WithClass { get; set; } = default!;
}
public class ClassWithoutDefaultConstructor
{
public string Name = string.Empty;
public ClassWithoutDefaultConstructor(string name)
{
this.Name = name;
}
}
public class ClassWithDefaultConstructor
{
public string Name = string.Empty;
public ClassWithDefaultConstructor(string name)
{
this.Name = name;
}
public ClassWithDefaultConstructor()
: this(string.Empty)
{
}
}
If you don't want to create an instance with default behavior, set ReconstructMember
of TinyhandObject
to false [TinyhandObject(ReconstructMember = false)]
.
Reuse Instance
Tinyhand will reuse an instance if its members have valid values. The type of the instance to be reused must have a TinyhandObject
attribute.
By adding [Reuse(true)]
or [Reuse(false)]
to member attributes, you can change the behavior of whether an instance is reused or not.
[TinyhandObject(ReuseMember = true)]
public partial class ReuseTestClass
{
[Key(0)]
[Reuse(false)]
public ReuseObject ObjectToCreate { get; set; } = new("create");
[Key(1)]
public ReuseObject ObjectToReuse { get; set; } = new("reuse");
[IgnoreMember]
public bool Flag { get; set; } = false;
}
[TinyhandObject(ImplicitKeyAsName = true)]
public partial class ReuseObject
{
public ReuseObject()
: this(string.Empty)
{
}
public ReuseObject(string name)
{
this.Name = name;
this.Length = name.Length;
}
[IgnoreMember]
public string Name { get; set; } // Not a serialization target
public int Length { get; set; }
}
public class ReuseTest
{
public void Test()
{
var t = new ReuseTestClass();
t.Flag = true;
// t2.Flag == true
// t2.ObjectToCreate.Name == "create", t2.ObjectToCreate.Length == 6
// t2.ObjectToReuse.Name == "reuse", t2.ObjectToReuse.Length == 5
var t2 = TinyhandSerializer.Deserialize<ReuseTestClass>(TinyhandSerializer.Serialize(t)); // Reuse member
// t2.Flag == false
// t2.ObjectToCreate.Name == "", t2.ObjectToCreate.Length == 6 // Note that Name is not a serialization target.
// t2.ObjectToReuse.Name == "reuse", t2.ObjectToReuse.Length == 5
t2 = TinyhandSerializer.DeserializeWith<ReuseTestClass>(t, TinyhandSerializer.Serialize(t)); // Reuse ReuseTestClass
// t2.Flag == true
// t2.ObjectToCreate.Name == "", t2.ObjectToCreate.Length == 6
// t2.ObjectToReuse.Name == "reuse", t2.ObjectToReuse.Length == 5
var reader = new Tinyhand.IO.TinyhandReader(TinyhandSerializer.Serialize(t));
t.Deserialize(ref reader, TinyhandSerializerOptions.Standard); ; // Same as above
}
}
If you don't want to reuse an instance with default behavior, set ReuseMember
of TinyhandObject
to false [TinyhandObject(ReuseMember = false)]
.
Use Service Provider
By default, Tinyhand requires default constructor for deserialization.
[TinyhandObject]
public partial class SomeClass
{
public SomeClass(ISomeService service)
{
}
}
Above code causes an exception during source code generation since Tinyhand doesn't know how to create an instance.
By setting TinyhandSerializer.ServiceProvider
and UseServiceProvider
to true, Tinyhand can create an instance without default constructor.
[TinyhandObject(UseServiceProvider = true)]
public partial class SomeClass
{
public SomeClass(ISomeService service)
{
}
}
TinyhandSerializer.ServiceProvider = someContainer;
var c = TinyhandSerializer.Deserialize<SomeClass>(b);
Union
Tinyhand supports serializing interface-typed and abstract class-typed objects. It behaves like XmlInclude
or ProtoInclude
. In Tinyhand these are called Union
. Only interfaces and abstracts classes are allowed to be annotated with TinyhandUnion
attributes. Unique union keys (int
) are required.
// Annotate inheritance types
[TinyhandUnion(0, typeof(UnionTestClassA))]
[TinyhandUnion(1, typeof(UnionTestClassB))]
public interface IUnionTestInterface
{
void Print();
}
[TinyhandObject]
public partial class UnionTestClassA : IUnionTestInterface
{
[Key(0)]
public int X { get; set; }
public void Print() => Console.WriteLine($"A: {this.X.ToString()}");
}
[TinyhandObject]
public partial class UnionTestClassB : IUnionTestInterface
{
[Key(0)]
public string Name { get; set; } = default!;
public virtual void Print() => Console.WriteLine($"B: {this.Name}");
}
public static class UnionTest
{
public static void Test()
{
var classA = new UnionTestClassA() { X = 10, };
var classB = new UnionTestClassB() { Name = "test", };
var b = TinyhandSerializer.Serialize((IUnionTestInterface)classA);
var i = TinyhandSerializer.Deserialize<IUnionTestInterface>(b);
i?.Print(); // A: 10
b = TinyhandSerializer.Serialize((IUnionTestInterface)classB);
i = TinyhandSerializer.Deserialize<IUnionTestInterface>(b);
i?.Print(); // B: test
}
}
Please be mindful that you cannot reuse the same keys in derived types that are already present in the parent type, as internally a single flat array or map will be used and thus cannot have duplicate indexes/keys.
Text Serialization
Tinyhand can serialize an object to Tinyhand text format .
// Serialize an object to string (UTF-16 text) and deserialize from it.
var myClass = new MyClass() { Age = 10, FirstName = "hoge", LastName = "huga", };
var st = TinyhandSerializer.SerializeToString(myClass);
var myClass2 = TinyhandSerializer.DeserializeFromString<MyClass>(st);
The result is
{
10, "hoge", "huga", null, null
}
UTF-8 version is available.
var utf8 = TinyhandSerializer.SerializeToUtf8(myClass);
var myClass3 = TinyhandSerializer.DeserializeFromUtf8<MyClass>(utf8);
Text Serialization is optional because it is 5 to 8 times slower than binary serialization.
Max length
You can set the maximum length of members by adding MaxLength
attribute and setting AddProperty
of Key
attribute.
[TinyhandObject]
public partial record MaxLengthClass
{
[Key(0, AddProperty = "Name")] // "Name" property will be created.
[MaxLength(3)] // The maximum length of Name property.
private string name = default!;
[Key(1, AddProperty = "Ids")]
[MaxLength(2)]
private int[] id = default!;
[Key(2, AddProperty = "Tags")]
[MaxLength(2, 3)] // The maximum length of an array and length of a string.
private string[] tags = default!;
public override string ToString()
=> $"""
Name: {this.Name}
Ids: {string.Join(',', this.Ids)}
Tags: {string.Join(',', this.Tags)}
""";
}
public static class MaxLengthTest
{
public static void Test()
{
var c = new MaxLengthClass();
c.Name = "ABCD"; // "ABC"
c.Ids = new int[] { 0, 1, 2, 3 }; // 0, 1,
c.Tags = new string[] { "aaa", "bbbb", "cccc" }; // "aaa", "bbb",
Console.WriteLine(c.ToString());
Console.WriteLine();
var st = TinyhandSerializer.SerializeToString(c);
st = """ "ABCD", {0, 1, 2, 3}, {"aaa", "bbbb", "cccc"} """;
var c2 = TinyhandSerializer.DeserializeFromString<MaxLengthClass>(st);
Console.WriteLine(c2!.ToString());
Console.WriteLine();
}
}
Versioning
Tinyhand serializer is version tolerant. If you serialize a version 1 object and deserialize it as version 2, the new members will be set to their default values. In the opposite direction, if you serialize a version 2 object and deserialize it as version 1, the new members will just be ignored.
[TinyhandObject]
public partial class VersioningClass1
{
[Key(0)]
public int Id { get; set; }
public override string ToString() => $" Version 1, ID: {this.Id}";
}
[TinyhandObject]
public partial class VersioningClass2
{
[Key(0)]
public int Id { get; set; }
[Key(1)]
[DefaultValue("John")]
public string Name { get; set; } = default!;
public override string ToString() => $" Version 2, ID: {this.Id} Name: {this.Name}";
}
public static class VersioningTest
{
public static void Test()
{
var v1 = new VersioningClass1() { Id = 1, };
Console.WriteLine("Original Version 1:");
Console.WriteLine(v1.ToString());// Version 1, ID: 1
var v12 = TinyhandSerializer.Deserialize<VersioningClass2>(TinyhandSerializer.Serialize(v1))!;
Console.WriteLine("Serialize v1 and deserialize as v2:");
Console.WriteLine(v12.ToString());// Version 2, ID: 1 Name: John (Default value is set)
Console.WriteLine();
var v2 = new VersioningClass2() { Id = 2, Name = "Fuga", };
Console.WriteLine("Original Version 2:");
Console.WriteLine(v2.ToString());// Version 2, ID: 2 Name: Fuga
var v21 = TinyhandSerializer.Deserialize<VersioningClass1>(TinyhandSerializer.Serialize(v2))!;
Console.WriteLine("Serialize v2 and deserialize as v1:");
Console.WriteLine(v21.ToString());// Version 1, ID: 2 (Name ignored)
}
}
Lock object
To acquire a mutual-exclusion lock during serialization and deserialization, add a LockObject
property.
[TinyhandObject(LockObject = "syncObject")]
public partial class LockObjectClass
{
[Key(0)]
public int X { get; set; }
private object syncObject = new();
}
Generated code is
static void ITinyhandSerialize<LockObjectClass>.Serialize(ref TinyhandWriter writer, scoped ref LockObjectClass? v, TinyhandSerializerOptions options)
{
if (v == null)
{
writer.WriteNil();
return;
}
lock (v.syncObject)
{
if (!options.IsSignatureMode) writer.WriteArrayHeader(1);
writer.Write(v.X);
}
}
Serialization Callback
[TinyhandObject]
public partial class SampleCallback
{
[Key(0)]
public int Key { get; set; }
[TinyhandOnSerializing]
public void OnSerializing()
{
Console.WriteLine("OnSerializing");
}
[TinyhandOnDeserialized]
public void OnDeserialized()
{
Console.WriteLine("OnDeserialized");
}
[TinyhandOnReconstructed]
public void OnReconstructed()
{
Console.WriteLine("OnReconstructed");
}
}
Deep copy
You can easily create a deep copy of the object by simply writing this code TinyhandSerializer.Clone(obj)
.
[TinyhandObject(ExplicitKeyOnly = true)]
public partial class DeepCopyClass
{
public int Id { get; set; }
public string[] Name { get; set; } = new string[] { "A", "B", };
public UnknownClass? UnknownClass { get; set; }
public KnownClass? KnownClass { get; set; }
}
public class UnknownClass
{
}
[TinyhandObject]
public partial class KnownClass
{
[Key(0)]
public float?[] Single { get; init; } = new float?[] { 0, 1, null, };
}
public static class DeepCopyTest
{
public static void Test()
{
var c = new DeepCopyClass();
c.UnknownClass = new();
c.KnownClass = new();
var d = TinyhandSerializer.Clone(c);
c.Name[1] = "C";
Debug.Assert(c.Name[1] != d.Name[1]); // c.Name and d.Name are different since d is a deep copy.
Debug.Assert(d.UnknownClass == null); // UnknownClass is ignored since Tinyhand doesn't know how to create a deep copy of UnknownClass.
Debug.Assert(d.KnownClass != null); // Tinyhand can handle a class with TinyhandObjectAttribute.
var e = TinyhandSerializer.Deserialize<DeepCopyClass>(TinyhandSerializer.Serialize(c)); // Almost the same as above, but Clone() is much faster.
}
}
TinyhandSerializer.Clone(obj)
is almost the same as TinyhandSerializer.Deserialize<Class>(TinyhandSerializer.Serialize(obj))
, but Clone()
is much faster.
Method | Mean | Error | StdDev | Median | Gen 0 | Gen 1 | Gen 2 | Allocated |
---|---|---|---|---|---|---|---|---|
Clone_Raw | 38.74 ns | 0.312 ns | 0.448 ns | 38.66 ns | 0.0421 | - | - | 176 B |
Clone_SerializeDeserialize | 282.87 ns | 3.473 ns | 4.636 ns | 278.95 ns | 0.0534 | - | - | 224 B |
Clone_Clone | 48.72 ns | 1.020 ns | 1.397 ns | 48.86 ns | 0.0421 | - | - | 176 B |
Built-in supported types
These types can serialize by default:
-
Primitives (
int
,string
, etc...),Enum
s,Nullable<>
,Lazy<>
-
TimeSpan
,DateTime
,DateTimeOffset
-
Guid
,Uri
,Version
,StringBuilder
-
BigInteger
,Complex
-
Array[]
,Array[,]
,Array[,,]
,Array[,,,]
,ArraySegment<>
,BitArray
-
KeyValuePair<,>
,Tuple<,...>
,ValueTuple<,...>
-
ArrayList
,Hashtable
-
List<>
,LinkedList<>
,Queue<>
,Stack<>
,HashSet<>
,ReadOnlyCollection<>
,SortedList<,>
-
IList<>
,ICollection<>
,IEnumerable<>
,IReadOnlyCollection<>
,IReadOnlyList<>
-
Dictionary<,>
,IDictionary<,>
,SortedDictionary<,>
,ILookup<,>
,IGrouping<,>
,ReadOnlyDictionary<,>
,IReadOnlyDictionary<,>
-
ObservableCollection<>
,ReadOnlyObservableCollection<>
-
ISet<>
, -
ConcurrentBag<>
,ConcurrentQueue<>
,ConcurrentStack<>
,ConcurrentDictionary<,>
-
Immutable collections (
ImmutableList<>
, etc) -
Custom implementations of
ICollection<>
orIDictionary<,>
with a parameterless constructor -
Custom implementations of
IList
orIDictionary
with a parameterless constructor
LZ4 Compression
Tinyhand has LZ4 compression support.
var b = TinyhandSerializer.Serialize(myClass, TinyhandSerializerOptions.Lz4);
var myClass2 = TinyhandSerializer.Deserialize<MyClass>(b, TinyhandSerializerOptions.Standard.WithCompression(TinyhandCompression.Lz4)); // Same as TinyhandSerializerOptions.Lz4
Non-Generic API
var myClass = (MyClass)TinyhandSerializer.Reconstruct(typeof(MyClass));
var b = TinyhandSerializer.Serialize(myClass.GetType(), myClass);
var myClass2 = TinyhandSerializer.Deserialize(typeof(MyClass), b);
Custom formatter
To create an custom formatter:
-
Create a formatter and register it with BuiltinResolver.
-
Source generator needs to be informed that the formatter exists. Register it with FormatterResolver.
Principles for creating a custom formatter:
- To improve performance, consider encapsulating everything within a single element (such as Binary).
- If splitting into multiple elements, use Array or Map. Simply listing elements can lead to data structure corruption when Arrays or Maps are used at a higher level.
public sealed class IPEndPointFormatter : ITinyhandFormatter<IPEndPoint>
{
public static readonly IPEndPointFormatter Instance = new IPEndPointFormatter();
public void Serialize(ref TinyhandWriter writer, IPEndPoint? value, TinyhandSerializerOptions options)
{// Nil or Bin8(Address, Port(4))
if (value == null)
{
writer.WriteNil();
return;
}
var span = writer.GetSpan(32);
if (value.Address.TryWriteBytes(span.Slice(2), out var written))
{
span[0] = MessagePackCode.Bin8;
span[1] = (byte)(written + 4); // Address + Port(4)
BitConverter.TryWriteBytes(span.Slice(2 + written), value.Port);
writer.Advance(2 + written + 4);
}
else
{
writer.WriteNil();
return;
}
}
public IPEndPoint? Deserialize(ref TinyhandReader reader, TinyhandSerializerOptions options)
{
if (!reader.TryReadBytes(out var span) ||
span.Length < 4)
{
return null;
}
var port = BitConverter.ToInt32(span.Slice(span.Length - 4));
return new IPEndPoint(new IPAddress(span.Slice(0, span.Length - 4)), port);
}
public IPEndPoint Reconstruct(TinyhandSerializerOptions options)
{
return new IPEndPoint(IPAddress.None, 0);
}
public IPEndPoint? Clone(IPEndPoint? value, TinyhandSerializerOptions options) => value == null ? null : new IPEndPoint(new IPAddress(value.Address.GetAddressBytes()), value.Port);
}