MessagePack Serialization for High-Performance .NET Services
JSON is human-readable, ubiquitous, and perfectly adequate for most APIs. But when you're serializing millions of messages between internal services, or pushing real-time data over SignalR, the overhead of text-based serialization adds up. MessagePack is a binary serialization format that's typically 2-4x smaller and 5-10x faster than JSON — and the .NET library MessagePack-CSharp makes it straightforward to adopt.
Getting started
Install the NuGet package:
dotnet add package MessagePack
Annotate your types with [MessagePackObject] and mark each property with a [Key]:
[MessagePackObject]
public class SensorReading
{
[Key(0)]
public int DeviceId { get; set; }
[Key(1)]
public double Temperature { get; set; }
[Key(2)]
public DateTime Timestamp { get; set; }
}
The integer keys produce the smallest payloads. Each property is identified by its index rather than its name, so the serialized bytes contain no property name strings at all.
Serialize and deserialize:
var reading = new SensorReading
{
DeviceId = 42,
Temperature = 21.5,
Timestamp = DateTime.UtcNow
};
byte[] bytes = MessagePackSerializer.Serialize(reading);
var deserialized = MessagePackSerializer.Deserialize<SensorReading>(bytes);
String keys for flexibility
If you prefer readability in debugging tools or need looser coupling between versions, use string keys:
[MessagePackObject(keyAsPropertyName: true)]
public class SensorReading
{
public int DeviceId { get; set; }
public double Temperature { get; set; }
public DateTime Timestamp { get; set; }
}
This embeds property names in the payload, making it slightly larger but more resilient to reordering or adding properties.
Using with SignalR
SignalR supports MessagePack as a hub protocol out of the box. Install the package:
dotnet add package Microsoft.AspNetCore.SignalR.Protocols.MessagePack
Configure the server:
builder.Services.AddSignalR()
.AddMessagePackProtocol();
And the JavaScript client:
const connection = new signalR.HubConnectionBuilder()
.withUrl("/sensorHub")
.withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
.build();
For high-frequency real-time scenarios — live dashboards, game state sync, IoT telemetry — the bandwidth savings are significant.
Source generation for AOT
MessagePack-CSharp v3 introduced a source generator that eliminates the reflection-based resolver, making it compatible with Native AOT and trimming:
[MessagePackObject]
public partial class SensorReading
{
[Key(0)]
public int DeviceId { get; set; }
[Key(1)]
public double Temperature { get; set; }
}
Enable the source generator in your project file:
<PropertyGroup>
<MessagePackGeneratedResolverEnabled>true</MessagePackGeneratedResolverEnabled>
</PropertyGroup>
Then register the generated resolver:
var options = MessagePackSerializerOptions.Standard
.WithResolver(GeneratedResolver.Instance);
var bytes = MessagePackSerializer.Serialize(reading, options);
Union types (polymorphism)
MessagePack-CSharp supports discriminated unions using the [Union] attribute:
[Union(0, typeof(TextMessage))]
[Union(1, typeof(ImageMessage))]
public interface IChatMessage
{
string Sender { get; }
}
[MessagePackObject]
public class TextMessage : IChatMessage
{
[Key(0)]
public string Sender { get; set; } = "";
[Key(1)]
public string Content { get; set; } = "";
}
[MessagePackObject]
public class ImageMessage : IChatMessage
{
[Key(0)]
public string Sender { get; set; } = "";
[Key(1)]
public byte[] ImageData { get; set; } = [];
}
The integer tag in [Union] acts as the type discriminator in the serialized payload — compact and fast.
When to use MessagePack over JSON
MessagePack shines in these scenarios:
- Internal service-to-service communication where human readability isn't needed
- High-throughput messaging (event streams, queue payloads, gRPC alternatives)
- Real-time systems where payload size affects latency
- Caching — smaller serialized objects mean more entries in your Redis cache
Stick with JSON when you need:
- Public-facing APIs that consumers expect to be human-readable
- Browser consumption without a dedicated client library
- Debugging ease — JSON payloads are trivially inspectable
Inspecting MessagePack payloads
For debugging, convert binary payloads to JSON:
var json = MessagePackSerializer.ConvertToJson(bytes);
Console.WriteLine(json);
This produces a JSON representation of the MessagePack structure, which is invaluable when diagnosing serialization issues.
Wrapping up
MessagePack-CSharp is a mature, battle-tested library that drops into .NET applications with minimal ceremony. If your bottleneck is serialization throughput or payload size — particularly in internal services or real-time communication — it's a compelling alternative to JSON. Start with one internal boundary, benchmark the difference, and expand from there.