Skip to content

Repository files navigation

SharpPack

GitHub Actions

High-performance, zero-encoding, source-generated binary serializer for modern .NET.

SharpPack icon

SharpPack is an independently maintained fork of Cysharp/MemoryPack. It retains the original MemoryPack binary wire format while using a fully renamed API, context-owned formatter graphs, modern .NET 10 runtime targets, and aggressive performance optimizations. See NOTICE for attribution.

Its speed comes from a C#-specific binary format, modern .NET APIs, generic static formatter dispatch, and an incremental source generator. The format copies compatible C# memory directly where possible and does not require a special schema object.

Other than performance, SharpPack has these features.

  • Support modern I/O APIs (IBufferWriter<byte>, ReadOnlySpan<byte>, ReadOnlySequence<byte>)
  • Native AOT friendly Source Generator based code generation, no Dynamic CodeGen (IL.Emit)
  • Deserialize into existing instance
  • Polymorphism (Union) serialization
  • Limited version-tolerant (fast/default) and full version-tolerant support
  • Circular reference serialization
  • PipeWriter/Reader based streaming serialization
  • TypeScript code generation
  • Explicit formatter contexts for collectible AssemblyLoadContext and RPC graphs

Installation

SharpPack 1.x targets .NET 10 and uses C# 14.

Install the aggregate runtime and source-generator package from NuGet:

dotnet add package SharpPack --version 1.1.0

Versioned packages and symbols are also attached to GitHub Releases.

The runtime packages target net10.0. SharpPack.Generator targets netstandard2.0 only because Roslyn analyzers are loaded by the compiler host; that target does not add a netstandard runtime support promise.

Quick Start

Define a struct or class to be serialized and annotate it with the [SharpPackable] attribute and the partial keyword.

using SharpPack;

[SharpPackable]
public partial class Person
{
    public int Age { get; set; }
    public string Name { get; set; }
}

Serialization code is generated by the C# source generator feature which implements the ISharpPackable<T> interface. In Visual Studio you can check a generated code by using a shortcut Ctrl+K, R on the class name and select *.SharpPackFormatter.g.cs.

Call SharpPackSerializer.Serialize<T>/Deserialize<T> to serialize/deserialize an object instance.

var v = new Person { Age = 40, Name = "John" };

var bin = SharpPackSerializer.Serialize(v);
var val = SharpPackSerializer.Deserialize<Person>(bin);

Serialize supports byte[], IBufferWriter<byte> and Stream. Deserialize supports ReadOnlySpan<byte>, ReadOnlySequence<byte> and Stream. Serializer operations are generic-only.

The ordinary byte[] serializer path, with or without an explicit Context, retains an 8 KB unpinned first buffer per thread. Applications dominated by payloads around 64 KB can select the 80 KB high-throughput preset during startup, before the first retained serializer state is created:

SharpPackSerializer.ConfigureRuntime(
    SharpPackSerializerRuntimeOptions.HighThroughput);

You can also create SharpPackSerializerRuntimeOptions with any custom ThreadBufferSize from 1 through Array.MaxLength; the presets are only convenience choices. For example:

SharpPackSerializer.ConfigureRuntime(new()
{
    ThreadBufferSize = 128 * 1024,
    PinThreadBuffer = false,
});

The retained buffer is allocated once per thread that reaches an ordinary byte[] return path, so increasing it trades process memory for fewer pooled segments and copies. Direct raw-unmanaged, fixed-size and exact-size paths do not create or freeze this state. PinThreadBuffer is an advanced native-interop option; it does not normally improve managed serialization. Runtime options freeze when the first retained byte-array serializer state is created. IBufferWriter<byte>, Stream and Streaming paths continue to use their caller-owned or pooled buffers.

Collectible AssemblyLoadContext

The parameterless serializer APIs use process-lifetime generic formatter slots directly; there is no global default-context object or mutable formatter graph. This is the fastest path and is appropriate for application types loaded into the default AssemblyLoadContext. Using these APIs with a type from a collectible AssemblyLoadContext does not provide an unload guarantee.

Use an explicit SharpPackSerializerContext when formatter lifetime must follow a plugin or an RPC endpoint:

var context = new SharpPackSerializerContext();

var bin = SharpPackSerializer.Serialize(pluginValue, context);
var value = SharpPackSerializer.Deserialize<PluginDto>(bin, context);

The same context is propagated through the complete object graph, including arrays, lists, dictionaries, generated types, unions and custom formatters, except across raw-copy unmanaged struct interiors as described below. Collectible types and registered overrides always use the context-owned graph; an empty/configuration-only context may share type-only static slots for non-collectible types.

Build a frozen context when custom formatters or UTF-16 output are needed:

var context = new SharpPackSerializerContextBuilder()
    .Configure(SharpPackSerializerConfiguration.Utf16)
    .Register<IPluginContract>(new PluginContractFormatter())
    .Build();

There is no mutable global formatter provider. Release the context together with plugin values, assemblies, reflection objects and delegates before unloading the plugin AssemblyLoadContext.

See Formatter contexts and collectible AssemblyLoadContext for lifecycle rules, migration details and architecture. A complete executable example is available in sandbox/CollectibleAlcSample.

See SharpPack versus MemoryPack benchmarks for the reproducible NuGet-only comparison and current latency, throughput and allocation results. SharpPack 1.1.0 matches MemoryPack 1.21.4 for unmanaged byte[] serialization and is about 9% faster on the pre-sized unmanaged IBufferWriter<byte> path in the published ARM64 run. On the generated object graph, deserialization is about 2% faster, while byte[] serialization ranges from 1% to 7% slower and the 1024-item writer path is 3% slower. Managed allocations are identical, including zero serializer allocation on pre-sized writer paths. Separate unpinned 8/80/256 KB measurements found all three buffer sizes equivalent within 3%, supporting the 8 KB default.

Built-in supported types

These types can be serialized by default:

  • .NET primitives (byte, int, bool, char, double, etc.)
  • Unmanaged types (Any enum, Any user-defined struct which doesn't contain reference types)
  • string, decimal, Half, Int128, UInt128, Guid, Rune, BigInteger
  • TimeSpan, DateTime, DateTimeOffset, TimeOnly, DateOnly, TimeZoneInfo
  • Complex, Plane, Quaternion Matrix3x2, Matrix4x4, Vector2, Vector3, Vector4
  • Uri, Version, StringBuilder, Type, BitArray, CultureInfo
  • T[], T[,], T[,,], T[,,,], Memory<>, ReadOnlyMemory<>, ArraySegment<>, ReadOnlySequence<>
  • Nullable<>, Lazy<>, KeyValuePair<,>, Tuple<,...>, ValueTuple<,...>
  • List<>, LinkedList<>, Queue<>, Stack<>, HashSet<>, SortedSet<>, PriorityQueue<,>
  • Dictionary<,>, SortedList<,>, SortedDictionary<,>, ReadOnlyDictionary<,>
  • Collection<>, ReadOnlyCollection<>, ObservableCollection<>, ReadOnlyObservableCollection<>
  • IEnumerable<>, ICollection<>, IList<>, IReadOnlyCollection<>, IReadOnlyList<>, ISet<>
  • IDictionary<,>, IReadOnlyDictionary<,>, ILookup<,>, IGrouping<,>,
  • ConcurrentBag<>, ConcurrentQueue<>, ConcurrentStack<>, ConcurrentDictionary<,>, BlockingCollection<>
  • Immutable collections (ImmutableList<>, etc.) and interfaces (IImmutableList<>, etc.)

Define [SharpPackable] class / struct / record / record struct

[SharpPackable] can annotate any class, struct, record, record struct or interface. As in MemoryPack, an unmanaged struct or record struct is serialized directly from its memory layout. Member annotations, including [SharpPackCustomFormatter], ignore/include, constructors and callbacks, are not applied. To customize its representation, register a formatter for the whole unmanaged type in a SharpPackSerializerContext.

Otherwise, by default, [SharpPackable] serializes public instance properties or fields. You can use [SharpPackIgnore] to remove serialization target, [SharpPackInclude] promotes a private member to serialization target.

[SharpPackable]
public partial class Sample
{
    // these types are serialized by default
    public int PublicField;
    public readonly int PublicReadOnlyField;
    public int PublicProperty { get; set; }
    public int PrivateSetPublicProperty { get; private set; }
    public int ReadOnlyPublicProperty { get; }
    public int InitProperty { get; init; }
    public required int RequiredInitProperty { get; init; }

    // these types are not serialized by default
    int privateProperty { get; set; }
    int privateField;
    readonly int privateReadOnlyField;

    // use [SharpPackIgnore] to remove target of a public member
    [SharpPackIgnore]
    public int PublicProperty2 => PublicProperty + PublicField;

    // use [SharpPackInclude] to promote a private member to serialization target
    [SharpPackInclude]
    int privateField2;
    [SharpPackInclude]
    int privateProperty2 { get; set; }
}

SharpPack's code generator adds information about what members are serialized to the <remarks /> section. This can be viewed by hovering over the type with Intellisense.

image

All members must be sharppack-serializable, if not the code generator will emit an error.

image

SharpPack has 35 diagnostics rules (SHARPPACK001 to SHARPPACK035) to be defined comfortably.

If target type is defined SharpPack serialization externally and registered, use [SharpPackAllowSerialize] to silent diagnostics.

[SharpPackable]
public partial class Sample2
{
    [SharpPackAllowSerialize]
    public NotSerializableType? NotSerializableProperty { get; set; }
}

Member order is important, SharpPack does not serialize the member-name or other information, instead serializing fields in the order they are declared. If a type is inherited, serialization is performed in the order of parent → child. The order of members can not change for the deserialization. For the schema evolution, see the Version tolerant section.

The default order is sequential, but you can choose the explicit layout with [SharpPackable(SerializeLayout.Explicit)] and [SharpPackOrder()].

// serialize Prop0 -> Prop1
[SharpPackable(SerializeLayout.Explicit)]
public partial class SampleExplicitOrder
{
    [SharpPackOrder(1)]
    public int Prop1 { get; set; }
    [SharpPackOrder(0)]
    public int Prop0 { get; set; }
}

Constructor selection

SharpPack supports both parameterized and parameterless constructors. The selection of the constructor follows these rules. (Applies to classes and structs).

  • If there is [SharpPackConstructor], use it.
  • If there is no explicit constructor (including private), use a parameterless one.
  • If there is one parameterless/parameterized constructor (including private), use it.
  • If there are multiple constructors, then the [SharpPackConstructor] attribute must be applied to the desired constructor (the generator will not automatically choose one), otherwise the generator will emit an error.
  • If using a parameterized constructor, all parameter names must match corresponding member names (case-insensitive).
[SharpPackable]
public partial class Person
{
    public readonly int Age;
    public readonly string Name;

    // You can use a parameterized constructor - parameter names must match corresponding members name (case-insensitive)
    public Person(int age, string name)
    {
        this.Age = age;
        this.Name = name;
    }
}

// also supports record primary constructor
[SharpPackable]
public partial record Person2(int Age, string Name);

public partial class Person3
{
    public int Age { get; set; }
    public string Name { get; set; }

    public Person3()
    {
    }

    // If there are multiple constructors, then [SharpPackConstructor] should be used
    [SharpPackConstructor]
    public Person3(int age, string name)
    {
        this.Age = age;
        this.Name = name;
    }
}

Serialization callbacks

When serializing/deserializing, SharpPack can invoke a before/after event using the [SharpPackOnSerializing], [SharpPackOnSerialized], [SharpPackOnDeserializing], [SharpPackOnDeserialized] attributes. It can annotate both static and instance (non-static) methods, and public and private methods.

[SharpPackable]
public partial class MethodCallSample
{
    // method call order is static -> instance
    [SharpPackOnSerializing]
    public static void OnSerializing1()
    {
        Console.WriteLine(nameof(OnSerializing1));
    }

    // also allows private method
    [SharpPackOnSerializing]
    void OnSerializing2()
    {
        Console.WriteLine(nameof(OnSerializing2));
    }

    // serializing -> /* serialize */ -> serialized
    [SharpPackOnSerialized]
    static void OnSerialized1()
    {
        Console.WriteLine(nameof(OnSerialized1));
    }

    [SharpPackOnSerialized]
    public void OnSerialized2()
    {
        Console.WriteLine(nameof(OnSerialized2));
    }

    [SharpPackOnDeserializing]
    public static void OnDeserializing1()
    {
        Console.WriteLine(nameof(OnDeserializing1));
    }

    // Note: instance method with SharpPackOnDeserializing, that not called if instance is not passed by `ref`
    [SharpPackOnDeserializing]
    public void OnDeserializing2()
    {
        Console.WriteLine(nameof(OnDeserializing2));
    }

    [SharpPackOnDeserialized]
    public static void OnDeserialized1()
    {
        Console.WriteLine(nameof(OnDeserialized1));
    }

    [SharpPackOnDeserialized]
    public void OnDeserialized2()
    {
        Console.WriteLine(nameof(OnDeserialized2));
    }
}

Callbacks allows parameterless method and ref reader/writer, ref T value method. For example, ref callbacks can write/read custom header before serialization process.

[SharpPackable]
public partial class EmitIdData
{
    public int MyProperty { get; set; }

    [SharpPackOnSerializing]
    static void WriteId<TBufferWriter>(ref SharpPackWriter<TBufferWriter> writer, ref EmitIdData? value)
        where TBufferWriter : IBufferWriter<byte> // .NET Standard 2.1, use where TBufferWriter : class, IBufferWriter<byte>
    {
        writer.WriteUnmanaged(Guid.NewGuid()); // emit GUID in header.
    }

    [SharpPackOnDeserializing]
    static void ReadId(ref SharpPackReader reader, ref EmitIdData? value)
    {
        // read custom header before deserialize
        var guid = reader.ReadUnmanaged<Guid>();
        Console.WriteLine(guid);
    }
}

Serialize external types

Implement SharpPackFormatter<T> and register the closed formatter while building a context:

var context = new SharpPackSerializerContextBuilder()
    .Register<ExternalType>(new ExternalTypeFormatter())
    .Build();

Registrations are frozen by Build and are isolated to that context. A generated wrapper type remains the simplest option when you control the RPC contract.

Packages

  • SharpPack — runtime plus source generator
  • SharpPack.Core — runtime primitives and formatter graph
  • SharpPack.Generator — Roslyn incremental source generator
  • SharpPack.StreamingPipeReader/PipeWriter and streaming APIs

TypeScript

SharpPack supports TypeScript code generation. It generates class and serialization code from C#, In other words, you can share types with the Browser without using OpenAPI, proto, etc.

Code generation is integrated with Source Generator, the following options(SharpPackGenerator_TypeScriptOutputDirectory) set the output directory for TypeScript code. Runtime code is output at the same time, so no additional dependencies are required.

<!-- output sharppack TypeScript code to directory -->
<ItemGroup>
    <CompilerVisibleProperty Include="SharpPackGenerator_TypeScriptOutputDirectory" />
</ItemGroup>
<PropertyGroup>
    <SharpPackGenerator_TypeScriptOutputDirectory>$(MSBuildProjectDirectory)\wwwroot\js\sharppack</SharpPackGenerator_TypeScriptOutputDirectory>
</PropertyGroup>

A C# SharpPackable type must be annotated with [GenerateTypeScript].

[SharpPackable]
[GenerateTypeScript]
public partial class Person
{
    public required Guid Id { get; init; }
    public required int Age { get; init; }
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public required DateTime DateOfBirth { get; init; }
    public required Gender Gender { get; init; }
    public required string[] Emails { get; init; }
}

public enum Gender
{
    Male, Female, Other
}

Runtime code and TypeScript type will be generated in the target directory.

image

The generated code is as follows, with simple fields and static methods for serialize/serializeArray and deserialize/deserializeArray.

import { SharpPackWriter } from "./SharpPackWriter.js";
import { SharpPackReader } from "./SharpPackReader.js";
import { Gender } from "./Gender.js"; 

export class Person {
    id: string;
    age: number;
    firstName: string | null;
    lastName: string | null;
    dateOfBirth: Date;
    gender: Gender;
    emails: (string | null)[] | null;

    constructor() {
        // snip...
    }

    static serialize(value: Person | null): Uint8Array {
        // snip...
    }

    static serializeCore(writer: SharpPackWriter, value: Person | null): void {
        // snip...
    }

    static serializeArray(value: (Person | null)[] | null): Uint8Array {
        // snip...
    }

    static serializeArrayCore(writer: SharpPackWriter, value: (Person | null)[] | null): void {
        // snip...
    }
    static deserialize(buffer: ArrayBuffer): Person | null {
        // snip...
    }

    static deserializeCore(reader: SharpPackReader): Person | null {
        // snip...
    }

    static deserializeArray(buffer: ArrayBuffer): (Person | null)[] | null {
        // snip...
    }

    static deserializeArrayCore(reader: SharpPackReader): (Person | null)[] | null {
        // snip...
    }
}

You can use this type like following.

let person = new Person();
person.id = crypto.randomUUID();
person.age = 30;
person.firstName = "foo";
person.lastName = "bar";
person.dateOfBirth = new Date(1999, 12, 31, 0, 0, 0);
person.gender = Gender.Other;
person.emails = ["[email protected]", "[email protected]"];

// serialize to Uint8Array
let bin = Person.serialize(person);

let blob = new Blob([bin.buffer], { type: "application/x-sharppack" })

let response = await fetch("http://localhost:5260/api",
    { method: "POST", body: blob, headers: { "Content-Type": "application/x-sharppack" } });

let buffer = await response.arrayBuffer();

// deserialize from ArrayBuffer 
let person2 = Person.deserialize(buffer);

TypeScript Type Mapping

There are a few restrictions on the types that can be generated. Among the primitives, char and decimal are not supported. Also, OpenGenerics type cannot be used.

C# TypeScript Description
bool boolean
byte number
sbyte number
int number
uint number
short number
ushort number
long bigint
ulong bigint
float number
double number
string string | null
Guid string In TypeScript, represents as string but serialize/deserialize as 16byte binary
DateTime Date DateTimeKind will be ignored
enum const enum long and ulong underlying type is not supported
T? T | null
T[] T[] | null
byte[] Uint8Array | null
: ICollection<T> T[] | null Supports all ICollection<T> implemented type like List<T>
: ISet<T> Set<T> | null Supports all ISet<T> implemented type like HashSet<T>
: IDictionary<K,V> Map<K, V> | null Supports all IDictionary<K,V> implemented type like Dictionary<K,V>.
[SharpPackable] class Supports class only
[SharpPackUnion] abstract class

[GenerateTypeScript] can only be applied to classes and is currently not supported by struct.

Configure import file extension and member name casing

In default, SharpPack generates file extension as .js like import { SharpPackWriter } from "./SharpPackWriter.js";. If you want to change other extension or empty, use SharpPackGenerator_TypeScriptImportExtension to configure it. Also the member name is automatically converted to camelCase. If you want to use original name, use SharpPackGenerator_TypeScriptConvertPropertyName to false.

<ItemGroup>
    <CompilerVisibleProperty Include="SharpPackGenerator_TypeScriptOutputDirectory" />
    <CompilerVisibleProperty Include="SharpPackGenerator_TypeScriptImportExtension" />
    <CompilerVisibleProperty Include="SharpPackGenerator_TypeScriptConvertPropertyName" />
    <CompilerVisibleProperty Include="SharpPackGenerator_TypeScriptEnableNullableTypes" />
</ItemGroup>
<PropertyGroup>
    <SharpPackGenerator_TypeScriptOutputDirectory>$(MSBuildProjectDirectory)\wwwroot\js\sharppack</SharpPackGenerator_TypeScriptOutputDirectory>
    <!-- allows empty -->
    <SharpPackGenerator_TypeScriptImportExtension></SharpPackGenerator_TypeScriptImportExtension>
    <!-- default is true -->
    <SharpPackGenerator_TypeScriptConvertPropertyName>false</SharpPackGenerator_TypeScriptConvertPropertyName>
    <!-- default is false -->
    <SharpPackGenerator_TypeScriptEnableNullableTypes>true</SharpPackGenerator_TypeScriptEnableNullableTypes>
</PropertyGroup>

SharpPackGenerator_TypeScriptEnableNullableTypes allows C# nullable annotations to be reflected in TypeScript code. The default is false, making everything nullable.

Streaming Serialization

SharpPack.Streaming provides SharpPackStreamingSerializer, which adds additional support for serializing and deserializing collections with streams.

public static class SharpPackStreamingSerializer
{
    public static async ValueTask SerializeAsync<T>(PipeWriter pipeWriter, int count, IEnumerable<T> source, int flushRate = 4096, CancellationToken cancellationToken = default)
    public static async ValueTask SerializeAsync<T>(Stream stream, int count, IEnumerable<T> source, int flushRate = 4096, CancellationToken cancellationToken = default)
    public static async IAsyncEnumerable<T?> DeserializeAsync<T>(PipeReader pipeReader, int bufferAtLeast = 4096, int readMinimumSize = 8192, [EnumeratorCancellation] CancellationToken cancellationToken = default)
    public static IAsyncEnumerable<T?> DeserializeAsync<T>(Stream stream, int bufferAtLeast = 4096, int readMinimumSize = 8192, CancellationToken cancellationToken = default)
}

Custom formatter API

Implement SharpPackFormatter<T> and register it through SharpPackSerializerContextBuilder. There is no process-wide formatter provider.

var context = new SharpPackSerializerContextBuilder()
    .Register<Skelton>(new SkeltonFormatter())
    .Build();

SharpPackWriterOptionalStatePool.Rent(context) and SharpPackReaderOptionalStatePool.Rent(context) are available when using the low-level reader/writer API directly.

RPC

For length-prefixed transports, SharpPack.Streaming exposes SerializeFrameAsync and DeserializeFrameAsync. The frame boundary is transport metadata and does not alter the SharpPack payload.

Native AOT

Generated formatters and the generic runtime path are designed for Native AOT. Reflection-based cold shape discovery must be preserved when trimming an application that relies on runtime generic shapes.

Closed generic shapes used only as serializer roots, such as a custom T[], may not otherwise have native generic code rooted by a generated model. Reference their closed formatter statically, or register it through a Context during startup. For example, a custom public factory can be rooted without reflection by using SharpPackSerializerContextBuilder.RegisterFactory<T, TFactory>(). The default generated factories are explicit static interface implementations and are preserved automatically. A public CreateFormatter discovered only by reflection remains a JIT compatibility path; NativeAOT callers should use the static registration API.

Binary wire format specification

The type of T defined in Serialize<T> and Deserialize<T> is called C# schema. SharpPack format is not self-described format. Deserialize requires the corresponding C# schema. These types exist as internal representations of binaries, but types cannot be determined without a C# schema.

Endian must be Little Endian. However, reference C# implementation does not care about endianness so can not use on big-endian machine. However, modern computers are usually little-endian.

There are eight types of format.

  • Unmanaged struct
  • Object
  • Version Tolerant Object
  • Circular Reference Object
  • Tuple
  • Collection
  • String
  • Union

Unmanaged struct

Unmanaged struct is C# struct that doesn't contain reference types, similar constraint of C# Unmanaged types. Serializing struct layout as it is, includes padding.

Object

(byte memberCount, [values...])

Object has 1byte unsigned byte as member count in header. Member count allows 0 to 249, 255 represents object is null. Values store sharppack value for the number of member count.

Version Tolerant Object

(byte memberCount, [varint byte-length-of-values...], [values...])

Version Tolerant Object is similar as Object but has byte length of values in header. varint follows these spec, first sbyte is value or typeCode and next X byte is value. 0 to 127 = unsigned byte value, -1 to -120 = signed byte value, -121 = byte, -122 = sbyte, -123 = ushort, -124 = short, -125 = uint, -126 = int, -127 = ulong, -128 = long.

Circular Reference Object

(byte memberCount, [varint byte-length-of-values...], varint referenceId, [values...])
(250, varint referenceId)

Circular Reference Object is similar as Version Tolerant Object but if memberCount is 250, next varint(unsigned-int32) is referenceId. If not, after byte-length-of-values, varint referenceId is written.

Tuple

(values...)

Tuple is fixed-size, non-nullable value collection. In .NET, KeyValuePair<TKey, TValue> and ValueTuple<T,...> are serialized as Tuple.

Collection

(int length, [values...])

Collection has 4 byte signed integer as data count in header, -1 represents null. Values store sharppack value for the number of length.

String

(int utf16-length, utf16-value)
(int ~utf8-byte-count, int utf16-length, utf8-bytes)

String has two-forms, UTF16 and UTF8. If first 4byte signed integer is -1, represents null. 0, represents empty. UTF16 is same as collection(serialize as ReadOnlySpan<char>, utf16-value's byte count is utf16-length * 2). If first signed integer <= -2, value is encoded by UTF8. utf8-byte-count is encoded in complement, ~utf8-byte-count to retrieve count of bytes. Next signed integer is utf16-length, it allows -1 that represents unknown length. utf8-bytes store bytes for the number of utf8-byte-count.

Union

(byte tag, value)
(250, ushort tag, value)

First unsigned byte is tag that for discriminated value type or flag, 0 to 249 represents tag, 250 represents next unsigned short is tag, 255 represents union is null.

License

This library is licensed under the MIT License.

About

High-performance MemoryPack-compatible binary serializer for modern .NET

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages