Nerdbank.MessagePack

repository·main·Indexed 41 days ago

https://github.com/aarnott/nerdbank.messagepack

A 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.

Tokens
38.7K
Snippets
97
Records
159
Agent score
51%

What's inside Nerdbank.MessagePack

  1. Overview of Nerdbank.MessagePack

    main
    Nerdbank.MessagePack is a modern, fast, and NativeAOT-compatible MessagePack serialization library for .NET and .NET Framework. It is designed as a successor to MessagePack-CSharp, offering improved user-friendliness and additional features while maintaining performance on par with highly tuned libraries. It supports high-performance scenarios through ref-based serialization, async I/O, and streaming deserialization.
  2. Overview of Nerdbank.MessagePack features

    main

    Nerdbank.MessagePack is a high-performance MessagePack serialization library for .NET designed with several key developer-centric features:

    • Contractless by default: No attributes are required on data types to enable serialization, though they can be used for customization.
    • Modern C# Support: Fully supports required and init properties, record classes/structs, and primary constructors.
    • Advanced Deserialization:
      • Polymorphic deserialization: Supports derived types, including duck-typed polymorphic serialization.
      • Targeted deserialization: Use LINQ expressions to deserialize only specific fragments of data.
      • Streaming: Supports true async and endless streaming deserialization for large or continuous data sequences.
    • Data Integrity & Evolution:
      • Forward compatibility: Retain unrecognized data during a deserialize/re-serialize cycle.
      • Reference preservation: Optionally preserve reference equality and handle cyclical references.
      • Structural equality: Provides deep by-value equality and hashing for arbitrary types.
    • Runtime Compatibility: Optimized for NativeAOT, trimmed environments, SignalR, ASP.NET Core MVC, and Unity.
    • Security: Built with secure defaults to protect against untrusted data, including rejection of multiple values for the same property.
  3. Benefits of Nerdbank.MessagePack.AspNetCoreMvcFormatter

    main

    Using this library provides several advantages over the default JSON protocol in ASP.NET MVC:

    • High Performance: Utilizes Nerdbank.MessagePack for fast, compact binary serialization.
    • Type Safety: Leverages MessagePack's type-safe serialization capabilities.
    • Easy Integration: Provides simple extension methods for service registration.
    • Full SignalR Support: Implements all SignalR message types, ensuring compatibility with real-time features.
  4. Key features of Nerdbank.MessagePack

    main

    Nerdbank.MessagePack is a high-performance MessagePack serialization library designed for modern .NET development. Key capabilities include:

    • NativeAOT & Trim Friendly: Works seamlessly in NativeAOT, trimmed, and Unity environments.
    • Minimal Configuration: No attributes are required on data types by default, though [GenerateShape] is recommended for performance.
    • Modern C# Support: Supports record classes/structs, required and init properties, and primary constructors.
    • Advanced Serialization: Supports polymorphic deserialization (unions), streaming deserialization, and targeted deserialization using LINQ expressions.
    • Data Integrity: Features forward-compatible data retention (retaining unrecognized properties) and optional preservation of reference equality.
    • Safety: Designed with security for untrusted data and contains no mutable statics, making it safe for multi-tenant or complex process environments.
  5. Install Nerdbank.MessagePack

    main
    Nerdbank.MessagePack is a modern, fast, and NativeAOT-compatible MessagePack serialization library for .NET. It is designed to work seamlessly in NativeAOT, trimmed, SignalR, ASP.NET Core MVC, and Unity environments. It supports modern C# features like required and init properties, record classes/structs, and primary constructors.
  6. Register runtime derived types

    main

    While type shapes are the preferred method for defining unions, runtime derived type registration is a useful alternative when:

    1. The defined shapes do not express any derived type information.
    2. You need to override derived type information because it is incorrect or incomplete (e.g., in an extensible application where you don't have complete type information at compile-time).
  7. How custom converters interact with Union types

    main

    The behavior of a custom converter when applied to a union type depends on where the attribute is applied:

    Applied to the Base Type

    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.

    Applied to a Member or Parameter

    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.

  8. Choose between Synchronous and Asynchronous (de)serialization

    main

    The choice between synchronous and asynchronous APIs involves a trade-off between raw speed and memory pressure.

    Synchronous (de)serialization

    • Performance: Fastest available APIs.
    • Allocations: Minimal memory allocations. The library strives to be allocation-free.
    • Note: While 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.

    Asynchronous (de)serialization

    • Performance: Slower than synchronous APIs (ranging from slightly to dramatically slower).
    • Memory Pressure: Reduces total memory pressure because the entire serialized representation does not necessarily need to be in memory at once.
    • Caveat: Memory pressure improvements are not guaranteed for very large atomic values (like long strings or large byte[] buffers), as these must still be fully in memory during the operation.
  9. NBMsgPack031: Converters should read or write exactly one msgpack structure

    main

    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).

    • If writing nothing: You may write an empty array header.
    • If writing multiple values: You must lead with an array or map header that exactly predicts the number of values that will follow.
    • If reading multiple values: You must first read the header (e.g., 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);
        }
    }
  10. Use surrogate types for unserializable types

    main

    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.

    • Surrogate Type: A serializable representation of your original type. It is recommended to use a record struct to minimize allocation costs. The surrogate must have at least internal visibility.
    • Marshaler: A class implementing 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);
    }
  11. Avoid loading arbitrary types

    main

    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.

  12. Define required properties for deserialization

    main

    A property or field is considered required during deserialization if:

    1. It uses the C# required modifier.
    2. It appears as a parameter in a deserializing constructor without a default value.

    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.