ZeroAlloc.Serialisation is a shared IBufferWriter<byte>-based serialisation library for the ZeroAlloc ecosystem. It provides a typed ISerializer<T> interface with a Roslyn source generator that emits AOT-safe, closed-generic implementations per annotated type — no reflection, no [RequiresDynamicCode] on hot paths.
Works with MemoryPack, MessagePack, and System.Text.Json.
The source generator is bundled into the main package — a single PackageReference is all you need:
# Core interface + bundled source generator
dotnet add package ZeroAlloc.Serialisation
# Pick your backend(s)
dotnet add package ZeroAlloc.Serialisation.MemoryPack
dotnet add package ZeroAlloc.Serialisation.MessagePack
dotnet add package ZeroAlloc.Serialisation.SystemTextJsonThe standalone
ZeroAlloc.Serialisation.Generatorpackage is still published for backwards compatibility with existing direct PackageReferences, but new consumers should reference onlyZeroAlloc.Serialisation.
// 1. Annotate your type
[MemoryPackable]
[ZeroAllocSerializable(SerializationFormat.MemoryPack)]
public partial class OrderCreated
{
public Guid OrderId { get; set; }
public string Product { get; set; } = "";
}
// 2. The generator emits OrderCreatedSerializer : ISerializer<OrderCreated>
// Register it with DI:
services.AddOrderCreatedSerializer();
// 3. Use it — writes directly to an IBufferWriter<byte>, zero intermediate allocation
public class OrderEventStore(ISerializer<OrderCreated> serializer)
{
public void Append(IBufferWriter<byte> writer, OrderCreated evt)
=> serializer.Serialize(writer, evt);
public OrderCreated? Read(ReadOnlySpan<byte> bytes)
=> serializer.Deserialize(bytes);
}A closed generic type such as Envelope<Order> is declared once per closed construction, on the assembly, and gets the same serializer, DI registration and dispatcher entry:
[assembly: ZeroAllocSerializable(typeof(Envelope<Order>), SerializationFormat.MemoryPack)]
services.AddEnvelopeOfOrderSerializer(); // ISerializer<Envelope<Order>>See Closed Generic Types.
When you need to serialize/deserialize by Type at runtime (e.g. in event sourcing infrastructure), use ISerializerDispatcher. The generator emits one SerializerDispatcher per assembly covering all annotated types:
// Register the dispatcher alongside per-type serializers:
services.AddOrderCreatedSerializer();
services.AddOrderShippedSerializer();
services.AddSerializerDispatcher(); // registers ISerializerDispatcher → SerializerDispatcher
// Inject and use:
public class EventStore(ISerializerDispatcher dispatcher)
{
public ReadOnlyMemory<byte> Serialize(object @event)
=> dispatcher.Serialize(@event, @event.GetType());
public object? Deserialize(ReadOnlyMemory<byte> data, Type eventType)
=> dispatcher.Deserialize(data, eventType);
}The generated SerializerDispatcher uses a compile-time switch — no reflection, no dictionary lookup, AOT-safe.
| Package | Description | TFM |
|---|---|---|
ZeroAlloc.Serialisation |
ISerializer<T>, ISerializerDispatcher, ZeroAllocSerializableAttribute |
netstandard2.1, net8–10 |
ZeroAlloc.Serialisation.Generator |
Roslyn source generator — legacy, already bundled inside ZeroAlloc.Serialisation; referencing both loads it twice |
netstandard2.0 |
ZeroAlloc.Serialisation.MemoryPack |
MemoryPack backend | net8–10 |
ZeroAlloc.Serialisation.MessagePack |
MessagePack backend | net8–10 |
ZeroAlloc.Serialisation.SystemTextJson |
System.Text.Json backend | net8–10 |
ZA.Serialisation is an abstraction layer — not a competing serializer. The honest comparison is whether the wrapper adds measurable overhead vs calling the raw library directly. .NET 10.0.7, BenchmarkDotNet v0.14.0.
Deserialize (wrapper is thin):
| Library | Raw | ZA wrapper | Overhead |
|---|---|---|---|
| MemoryPack | 48 ns / 64 B | 55 ns / 64 B | +16%, 0 B |
| MessagePack | 124 ns / 64 B | 183 ns / 96 B | +47%, +32 B |
| System.Text.Json | 303 ns / 64 B | 375 ns / 64 B | +23%, 0 B |
Serialize (IBufferWriter pattern adds measurable cost):
| Library | Raw | ZA wrapper | Overhead |
|---|---|---|---|
| MemoryPack | 75 ns / 48 B | 160 ns / 312 B | +114%, +264 B |
| MessagePack | 128 ns / 32 B | 215 ns / 312 B | +68%, +280 B |
| System.Text.Json | 226 ns / 48 B | 288 ns / 448 B | +27%, +400 B |
The serialize wrapper costs more because ISerializer<T>.Serialize takes an IBufferWriter<byte> — the buffer abstraction. The 264–400 B is the ArrayBufferWriter<byte> allocated fresh per call by the benchmark. The wrapper is fastest when the caller pools the buffer writer; a real application that pools writers amortises the overhead to ~0 per call.
Full methodology and guidance on when to use the wrapper vs raw libraries: docs/performance.md.
Generated serializers suppress [RequiresDynamicCode] and [RequiresUnreferencedCode] because T is resolved at generation time. The backend's own source generator (MemoryPack / MessagePack) has already emitted the formatter, so no dynamic code is required at runtime.
Base classes (MemoryPackSerializer<T>, etc.) carry the attributes for ad-hoc / non-AOT use.
For JSON clients, prefer Rest's own new SystemTextJsonSerializer(MyApiJsonContext.Default): it covers every request and response type in one serializer and is AOT-safe.
For a non-JSON backend, bridge ISerializer<T> into IRestSerializer with a short hand-written adapter (Rest 3.0 removed the built-in RestSerializerAdapter<T>; Rest 2.x users can keep using it). See docs/rest-integration.md for the full adapter and setup.
MIT