Overview of Nerdbank.MessagePack
mainref-based serialization, async I/O, and streaming deserialization.repository·main·Indexed 41 days ago
https://github.com/aarnott/nerdbank.messagepackA modern, high-performance MessagePack serialization library for .NET and .NET Framework designed for NativeAOT and trim compatibility. It supports polymorphic deserialization, streaming, and async I/O, and includes specialized integrations for ASP.NET Core SignalR, ASP.NET Core MVC, and Godot Engine.
ref-based serialization, async I/O, and streaming deserialization.Nerdbank.MessagePack is a high-performance MessagePack serialization library for .NET designed with several key developer-centric features:
required and init properties, record classes/structs, and primary constructors.Using this library provides several advantages over the default JSON protocol in ASP.NET MVC:
Nerdbank.MessagePack for fast, compact binary serialization.Nerdbank.MessagePack is a high-performance MessagePack serialization library designed for modern .NET development. Key capabilities include:
[GenerateShape] is recommended for performance.record classes/structs, required and init properties, and primary constructors.required and init properties, record classes/structs, and primary constructors.While type shapes are the preferred method for defining unions, runtime derived type registration is a useful alternative when:
The behavior of a custom converter when applied to a union type depends on where the attribute is applied:
If the converter is applied to the base type (via MessagePackConverterAttribute on the type or via runtime registration), it will only serialize and deserialize instances of that exact type. The value is automatically enveloped in a msgpack union structure with the appropriate type metadata. The converter does not need to handle derived types or metadata manually.
Note: A custom converter applied to an abstract or interface type will never run because objects can only be instances of concrete types.
If the converter is applied to a specific member or parameter, it takes total control over the serialization of that value. In this scenario, the custom converter is responsible for handling any union type metadata itself.
The choice between synchronous and asynchronous APIs involves a trade-off between raw speed and memory pressure.
Serialize<T>(T value, CancellationToken ct) allocates a byte[] for the return value, you can avoid this by using overloads that allow serializing directly to pooled buffers.byte[] buffers), as these must still be fully in memory during the operation.When implementing custom converters by deriving from Nerdbank.MessagePack.MessagePackConverter<T>, you must ensure that the Read and Write methods handle exactly one MessagePack structure.
A single structure can be a scalar value (like an integer) or a collection (like an array or a map).
ReadArrayHeader) and then process the elements. To maintain compatibility, you should either deserialize exactly the expected number of elements or use a loop to consume all elements in the structure (using reader.Skip() for unknown elements) to ensure the reader position is correctly advanced.public class MyTypeConverter : MessagePackConverter<MyType>
{
public override MyType Read(ref MessagePackReader reader, SerializationContext context)
{
int count = reader.ReadArrayHeader();
if (count != 2)
{
throw new MessagePackSerializationException("Expected 2 elements.");
}
int property1 = reader.ReadInt32();
short property2 = reader.ReadInt16();
return new MyType(property1, property2);
}
public override void Write(ref MessagePackWriter writer, in MyType value, SerializationContext context)
{
writer.WriteArrayHeader(2);
writer.Write(value.Property1);
writer.Write(value.Property2);
}
}When a type cannot be automatically serialized (e.g., due to private fields or incompatible types), you can define a surrogate type and a marshaler.
record struct to minimize allocation costs. The surrogate must have at least internal visibility.IMarshaler<TOriginal, TSurrogate> that handles the conversion between the original type and the surrogate. The marshaler must have at least internal visibility.Important Note on Nullability: If the original type is a reference type and the surrogate is a value type, specify a nullable surrogate type in the marshaler to ensure null identity is preserved correctly.
// 1. The original unserializable type
[TypeShape(Marshaler = typeof(MyMarshaler))]
public class MyType
{
private int _secret;
public MyType(int secret) => _secret = secret;
}
// 2. The surrogate (use a struct for performance)
internal record struct MyTypeSurrogate(int Secret);
// 3. The marshaler
internal class MyMarshaler : IMarshaler<MyType, MyTypeSurrogate>
{
public MyTypeSurrogate ToSurrogate(MyType original)
=> new MyTypeSurrogate(original._secret);
public MyType FromSurrogate(MyTypeSurrogate surrogate)
=> new MyType(surrogate.Secret);
}To prevent attackers from forcing the application to deserialize into unexpected, malicious types, Nerdbank.MessagePack does not support the deserialization of arbitrary types. Data must be deserialized into a known, closed set of types.
To allow data to dictate which derived type is activated for a base class, use Unions. While you can author a custom converter to support arbitrary type loading, it is strongly discouraged for security reasons.
A property or field is considered required during deserialization if:
required modifier.If a member is required, its value must be present in the MessagePack data for deserialization to succeed. You can override this requirement for individual members using PropertyShapeAttribute.IsRequired.