Each serializer and compressor is distributed as a separate NuGet package to avoid transitive dependency bloat.
Default serializer using System.Text.Json. No additional dependencies beyond .NET.
tracking.ForEntity<Product>(e => e
.UseJsonSerializer());Package: RayTree.Plugins.Serializers.Json
Features:
- UTF-8 encoding
- Supports polymorphic types
- Configurable via
JsonSerializerOptions
Binary serializer using protobuf-net. Compact messages, fast serialization.
tracking.ForEntity<Product>(e => e
.UseProtobufSerializer());Package: RayTree.Plugins.Serializers.Protobuf
Features:
- Binary format (smaller payload than JSON)
- Requires
[ProtoContract]and[ProtoMember]attributes on entity types - Schema evolution support (additive changes are backward-compatible)
Entity setup:
[ProtoContract]
public class Product
{
[ProtoMember(1)]
public int Id { get; set; }
[ProtoMember(2)]
public string Name { get; set; } = null!;
[ProtoMember(3)]
public decimal Price { get; set; }
}Binary serializer using MessagePack-CSharp. Faster than Protobuf for some scenarios.
tracking.ForEntity<Product>(e => e
.UseMessagePackSerializer());Package: RayTree.Plugins.Serializers.MessagePack
Features:
- Binary format
- Uses
ContractlessStandardResolver(no attributes required on entity types) - Extremely fast serialization/deserialization
Standard gzip compression using System.IO.Compression. Universal compatibility.
tracking.ForEntity<Product>(e => e
.UseGzipCompressor());Package: RayTree.Plugins.Compressors.Gzip
Best for: General-purpose compression, wide tool support.
Modern compression using System.IO.Compression. Better ratio than gzip.
tracking.ForEntity<Product>(e => e
.UseBrotliCompressor());Package: RayTree.Plugins.Compressors.Brotli
Best for: Text-heavy data where size matters more than CPU.
Ultra-fast compression using K4os.Compression.LZ4.
tracking.ForEntity<Product>(e => e
.UseLz4Compressor());Package: RayTree.Plugins.Compressors.Lz4
Best for: High-throughput scenarios where CPU time is critical.
Pass-through compressor (no compression).
tracking.ForEntity<Product>(e => e
.UseNoOpCompressor());Best for: Small messages, local testing, or when the message broker handles compression.
Each entity can have its own combination:
// High-throughput: fast serializer + fast compressor
tracking.ForEntity<Order>(e => e
.UseMessagePackSerializer()
.UseLz4Compressor());
// Storage-optimized: compact serializer + best compressor
tracking.ForEntity<AuditLog>(e => e
.UseProtobufSerializer()
.UseBrotliCompressor());
// Simple: JSON + Gzip
tracking.ForEntity<Product>(e => e
.UseJsonSerializer()
.UseGzipCompressor());The subscriber must use the same serializer and compressor as the publisher. The publisher serializes and compresses into MessageEnvelope.Payload; the subscriber decompresses and deserializes in the reverse order.
// Publisher
tracking.ForEntity<Product>(e => e
.UseProtobufSerializer()
.UseBrotliCompressor());
// Subscriber (must match!)
builder.Services
.AddChangeSubscriber(configuration)
.ForEntity<Product>(e => e
.UseSerializer(new ProtobufSerializerPlugin()) // same as publisher
.UseCompressor(new BrotliCompressorPlugin()) // same as publisher
.UseConsumer(myConsumer)
.OnInsert(async (change, ct) =>
{
var product = change.State; // typed Product
}));If no serializer is registered on the subscriber, change.State will be null and only the metadata fields (EntityId, ChangeType, CorrelationId, etc.) are available.
When all entities share the same serializer and compressor, set them globally on the subscriber builder:
builder.Services
.AddChangeSubscriber(configuration)
.UseSerializer(new JsonSerializerPlugin()) // applies to all entities
.UseCompressor(new GzipCompressorPlugin()) // applies to all entities
.ForEntity<Order>(e => e
.UseConsumer(orderConsumer)
.OnInsert(async (change, ct) => { /* ... */ }))
.ForEntity<Product>(e => e
.UseConsumer(productConsumer)
.OnInsert(async (change, ct) => { /* ... */ }));