Skip to main content

Serialization

Dispatch supports multiple serialization formats for messages. Choose based on your needs for performance, interoperability, and debugging.

Before You Start

  • .NET 8.0+ (or .NET 9/10 for latest features)
  • Install the required package:
    dotnet add package Excalibur.Dispatch
  • For alternative serializers, install the provider package (e.g., Excalibur.Dispatch.Serialization.MemoryPack)
  • Familiarity with middleware concepts and pipeline stages

Serializers

SerializerPackageBest For
System.Text.Json (default)Built-inCross-language, debugging
MemoryPackExcalibur.Dispatch.Serialization.MemoryPack.NET-only, max performance
MessagePackExcalibur.Dispatch.Serialization.MessagePackCross-language, compact
ProtobufExcalibur.Dispatch.Serialization.ProtobufSchema-based, gRPC compat

Configuration

Default (System.Text.Json)

services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
});

// Default: MemoryPack is used for internal serialization.
// To use System.Text.Json for patterns/hosting:
services.AddJsonSerialization();

Custom JSON Options

services.AddJsonSerialization(options =>
{
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.SerializerOptions.WriteIndented = false;
options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});

MemoryPack (Fastest)

dotnet add package Excalibur.Dispatch.Serialization.MemoryPack
services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
});

// Register MemoryPack as internal serializer
services.AddMemoryPackInternalSerialization();

// Messages must be MemoryPack-compatible
[MemoryPackable]
public partial record CreateOrderAction(
Guid OrderId,
string CustomerId,
List<OrderItem> Items) : IDispatchAction;

MessagePack

dotnet add package Excalibur.Dispatch.Serialization.MessagePack
services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
});

// Register MessagePack serialization
services.AddMessagePackSerialization(options =>
{
options.SerializerOptions = MessagePackSerializerOptions.Standard
.WithCompression(MessagePackCompression.Lz4BlockArray);
});

[MessagePackObject]
public class CreateOrderAction : IDispatchAction
{
[Key(0)] public Guid OrderId { get; set; }
[Key(1)] public string CustomerId { get; set; }
[Key(2)] public List<OrderItem> Items { get; set; }
}

Protobuf

dotnet add package Excalibur.Dispatch.Serialization.Protobuf
services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
});

// Register Protobuf via the pluggable serialization system
services.AddPluggableSerialization();

// Define in .proto file
// message CreateOrderAction {
// string order_id = 1;
// string customer_id = 2;
// repeated OrderItem items = 3;
// }

Compression

Enable Compression

Compression can be configured via serialization options:

services.AddJsonSerialization(options =>
{
// Configure compression settings on JSON options
options.SerializerOptions.WriteIndented = false; // Compact output
});

// Or configure compression at the transport level via the builder
services.AddDispatch(dispatch =>
{
dispatch.UseKafka(kafka =>
{
kafka.CompressionType(CompressionType.Gzip);
});
});

Encryption

Transport Encryption

Encryption is handled by the Excalibur.Dispatch.Security package, not the serializer:

services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
dispatch.UseMiddleware<MessageEncryptionMiddleware>();
});

// Configure encryption via IConfiguration
services.AddDispatchSecurity(configuration);

Field-Level Encryption

Field-level encryption uses the [EncryptedField] attribute on byte[] properties. For string data, serialize to bytes first:

using Excalibur.Dispatch.Compliance;

public class CustomerProjection
{
public string Id { get; set; }
public string Name { get; set; }

[EncryptedField]
public byte[] SocialSecurityNumber { get; set; }

[EncryptedField(Purpose = "pci-data")]
public byte[] CreditCardData { get; set; }
}

See Security for encryption architecture details.

AOT Compatibility

Source Generators

// Enable source generation for AOT
[JsonSerializable(typeof(CreateOrderAction))]
[JsonSerializable(typeof(OrderCreatedEvent))]
public partial class AppJsonContext : JsonSerializerContext { }

services.AddJsonSerialization(options =>
{
options.SerializerOptions.TypeInfoResolver = AppJsonContext.Default;
});

MemoryPack AOT

// MemoryPack is AOT-friendly by default
[MemoryPackable]
public partial record CreateOrderAction(...) : IDispatchAction;

Multi-Format Support

Per-Transport Serialization

Use transport routing with the pluggable serialization system to route different message types to transports that use different serializers:

services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);

dispatch.UseRouting(routing =>
{
routing.Transport
// Kafka: high-volume events
.Route<HighVolumeEvent>().To("kafka")
// External API: interop events
.Route<ExternalEvent>().To("http")
.Default("rabbitmq");
});
});

// Register pluggable serialization with multiple formats
services.AddPluggableSerialization();
services.AddMessagePackPluggableSerialization();

Pluggable Serialization Registry

// Register multiple serializers via the pluggable system
services.AddPluggableSerialization();
services.AddMessagePackPluggableSerialization(setAsCurrent: true);

// Or register manually via ISerializerRegistry
var registry = services.GetRequiredService<ISerializerRegistry>();
registry.Register(SerializerIds.MessagePack,
MessagePackSerializationExtensions.GetPluggableSerializer());

Custom Serializers

Implementing IMessageSerializer

using Excalibur.Dispatch.Abstractions.Serialization;

public class CustomSerializer : IMessageSerializer
{
public string SerializerName => "CustomFormat";
public string SerializerVersion => "1.0";

public byte[] Serialize<T>(T message)
{
// Your serialization logic
return CustomFormat.Serialize(message);
}

public T Deserialize<T>(byte[] data)
{
// Your deserialization logic
return CustomFormat.Deserialize<T>(data);
}
}

Registration

// Register your custom serializer via DI
services.AddSingleton<IMessageSerializer, CustomSerializer>();

Zero-Allocation Serialization

For high-throughput event sourcing scenarios, Dispatch provides IZeroAllocEventSerializer which uses Span<byte> and ArrayPool<byte> to eliminate allocations during serialization.

When to Use

ScenarioRecommendation
High-volume event storesUse ZeroAlloc
Event replay/projectionUse ZeroAlloc
Standard message dispatchStandard serializers sufficient
Cross-language systemsUse standard JSON

IZeroAllocEventSerializer Interface

public interface IZeroAllocEventSerializer : IEventSerializer
{
// Span-based serialization (zero-allocation)
int SerializeEvent(IDomainEvent domainEvent, Span<byte> buffer);
IDomainEvent DeserializeEvent(ReadOnlySpan<byte> data, Type eventType);
int GetEventSize(IDomainEvent domainEvent);

// Snapshot support
int SerializeSnapshot(object snapshot, Span<byte> buffer);
object DeserializeSnapshot(ReadOnlySpan<byte> data, Type snapshotType);
int GetSnapshotSize(object snapshot);
}

Usage Pattern

// 1. Get the serializer
var serializer = serviceProvider.GetRequiredService<IZeroAllocEventSerializer>();

// 2. Estimate buffer size
var size = serializer.GetEventSize(domainEvent);

// 3. Rent buffer from pool (zero allocation)
var buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
// 4. Serialize to rented buffer
var written = serializer.SerializeEvent(domainEvent, buffer);

// 5. Use the serialized data
await eventStore.AppendAsync(buffer.AsSpan(0, written), ct);
}
finally
{
// 6. Return buffer to pool
ArrayPool<byte>.Shared.Return(buffer);
}

Configuration

using Excalibur.Dispatch.Serialization;

services.AddDispatch(dispatch =>
{
dispatch.AddHandlersFromAssembly(typeof(Program).Assembly);
// Register SpanEventSerializer (uses MemoryPack by default)
dispatch.AddZeroAllocEventSerializer();
});

// Or with explicit pluggable serializer
services.AddSingleton<IZeroAllocEventSerializer>(sp =>
new SpanEventSerializer(sp.GetRequiredService<ISerializerRegistry>()));

SpanEventSerializer Implementation

The built-in SpanEventSerializer wraps the pluggable serialization infrastructure:

// SpanEventSerializer prefers MemoryPack for best Span support
// Falls back to current configured serializer if MemoryPack unavailable
public SpanEventSerializer(ISerializerRegistry registry)
{
_pluggable = registry.GetByName("MemoryPack")
?? registry.GetById(SerializerIds.MemoryPack)
?? registry.GetCurrent().Serializer;
}

Performance Characteristics

OperationStandard IEventSerializerIZeroAllocEventSerializer
Allocations per serialize1-3 byte[]0 (pooled)
Allocations per deserialize1 byte[] copy0 (span-based)
GC pressureModerateMinimal
Best forGeneral useHigh-throughput event stores

Integration with Event Stores

public class HighPerformanceEventStore : IEventStore
{
private readonly IZeroAllocEventSerializer _serializer;

public async Task AppendAsync(
Guid streamId,
IReadOnlyList<IDomainEvent> events,
CancellationToken ct)
{
foreach (var evt in events)
{
var size = _serializer.GetEventSize(evt);
var buffer = ArrayPool<byte>.Shared.Rent(size);
try
{
var written = _serializer.SerializeEvent(evt, buffer);
await PersistAsync(streamId, buffer.AsSpan(0, written), ct);
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
}
}

Backward Compatibility

IZeroAllocEventSerializer extends IEventSerializer, so it supports both patterns:

IZeroAllocEventSerializer serializer = ...;

// New Span-based API (zero-allocation)
int written = serializer.SerializeEvent(evt, buffer.AsSpan());

// Legacy byte[] API (still works)
byte[] data = serializer.SerializeEvent(evt);

Performance Comparison

SerializerSerializeDeserializeSizeAllocations
ZeroAlloc + MemoryPack0.8x0.9x1x0
MemoryPack1x1x1x1-2
MessagePack2.5x2x1.1x2-3
System.Text.Json4x3x1.5x3-5
Newtonsoft.Json6x5x1.5x5-8
Protobuf3x2.5x0.9x2-3

Relative performance - lower is better. Actual results vary by payload.

ZeroAlloc Benchmark Suite

For detailed benchmarks comparing ZeroAlloc vs JSON serialization, run:

cd benchmarks/Excalibur.Dispatch.Benchmarks
dotnet run -c Release --filter *SpanEventSerializer*

Benchmark Categories:

  • JSON vs ZeroAlloc comparison (small and large events)
  • Buffer pooling allocation verification
  • Event sourcing scenarios (100-event replay/append)
  • Round-trip benchmarks

See the Performance Best Practices for detailed optimization guidance.

Best Practices

ScenarioRecommendation
High-throughput event storesZeroAlloc + MemoryPack
Event replay/projectionsZeroAlloc + MemoryPack
.NET-only, high performanceMemoryPack
Cross-languageSystem.Text.Json or MessagePack
Schema evolutionProtobuf
DebuggingSystem.Text.Json with WriteIndented
Large payloadsEnable compression
Sensitive dataEnable encryption
AOT deploymentMemoryPack or System.Text.Json with source gen

Next Steps

See Also