LightProto 1.2.1

dotnet add package LightProto --version 1.2.1
                    
NuGet\Install-Package LightProto -Version 1.2.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="LightProto" Version="1.2.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="LightProto" Version="1.2.1" />
                    
Directory.Packages.props
<PackageReference Include="LightProto" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add LightProto --version 1.2.1
                    
#r "nuget: LightProto, 1.2.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package LightProto@1.2.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=LightProto&version=1.2.1
                    
Install as a Cake Addin
#tool nuget:?package=LightProto&version=1.2.1
                    
Install as a Cake Tool

LightProto 🚀

.NET NuGet downloads Build codecov CodeQL Advanced Codacy Badge GitHub Repo stars Size License

English | 简体中文

A high-performance, Native AOT–friendly, production-ready Protocol Buffers implementation for C#/.NET, powered by source generators.

Why LightProto? 🤔

protobuf-net is a popular Protocol Buffers implementation in .NET, but some scenarios (especially Native AOT) can be challenging due to runtime reflection and dynamic generation. LightProto addresses this with compile-time code generation and a protobuf-net–style API.

Features ✨

  • Source generator–powered serializers/parsers generated at compile time
  • AOT-friendly by design, no IL warnings
  • Minimum C# 9.0 requirement for broader compatibility (including Unity)
  • No third-party dependencies
  • protobuf-net–style Serializer API and familiar attributes
  • Performance is about 20% to 50% better than protobuf-net; see benchmarks below
  • Target frameworks: netstandard2.0, net8.0, net9.0, net10.0
  • Serialize to Stream or IBufferWriter<byte>, or use ToByteArray
  • ReadOnlySpan<byte>/ReadOnlySequence<byte>/Stream deserialization
  • Dynamic and non-generic serialization/deserialization APIs
  • RuntimeTypeModel-like APIs for dynamic message types
  • Surrogates supported

Rich built-in type support 🧰

  • .NET primitives (byte,sbyte, int,uint,long,ulong, bool, char, double, etc.)
  • 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, BitArray, CultureInfo
  • Nullable<>, Lazy<>
  • T[], List<>, LinkedList<>, Queue<>, Stack<>, HashSet<>, SortedSet<>
  • Dictionary<,>, SortedList<,>, SortedDictionary<,>, ReadOnlyDictionary<,>
  • Collection<>, ReadOnlyCollection<>, ObservableCollection<>, ReadOnlyObservableCollection<>
  • IEnumerable<>, ICollection<>, IList<>, IReadOnlyCollection<>, IReadOnlyList<>, ISet<>
  • IDictionary<,>, IReadOnlyDictionary<,>
  • ConcurrentBag<>, ConcurrentQueue<>, ConcurrentStack<>, ConcurrentDictionary<,>, BlockingCollection<>
  • ImmutableList<>, ImmutableArray<>, ImmutableHashSet<>, ImmutableDictionary<,>

Quick Start ⚡

Install from NuGet:

dotnet add package LightProto

Define your contracts (partial classes) using LightProto attributes:

using LightProto;

[ProtoContract]
public partial class Person
{
    [ProtoMember(1)]
    public string Name { get; set; } = string.Empty;

    [ProtoMember(2)]
    public int Age { get; set; }
}

var person = new Person { Name = "Alice", Age = 30 };

// Serialize to a byte[]
byte[] bytes = person.ToByteArray();
// person.ToByteArray(Person.ProtoWriter); // use this overload when targeting .netstandard2.0

// Or serialize to a Stream
using var stream = new MemoryStream();
Serializer.Serialize(stream, person);
// Serializer.Serialize(stream, person, Person.ProtoWriter); // use this overload when targeting .netstandard2.0

byte[] data = stream.ToArray();

// Deserialize from byte[] (ReadOnlySpan<byte> overload will be used)
Person fromBytes = Serializer.Deserialize<Person>(bytes);
// Person fromBytes = Serializer.Deserialize<Person>(bytes, Person.ProtoReader); // use this overload when targeting .netstandard2.0

// Or deserialize from Stream
using var input = new MemoryStream(data);
Person fromStream = Serializer.Deserialize<Person>(input);
// Person fromStream = Serializer.Deserialize<Person>(input, Person.ProtoReader); // use this overload when targeting .netstandard2.0

Migration from protobuf-net 🔁

Most code migrates by swapping the namespace and marking your types partial.

  1. Replace ProtoBuf with LightProto.
  2. Mark serializable types as partial.

Example:

- using ProtoBuf;
+ using LightProto;

[ProtoContract]
- public class Person
+ public partial class Person
{
    [ProtoMember(1)]
    public string Name { get; set; } = string.Empty;

    [ProtoMember(2)]
    public int Age { get; set; }
}

var myObject = new Person { Name = "Alice", Age = 30 };

// Serialization
var stream = new MemoryStream();
Serializer.Serialize(stream, myObject);
byte[] data = stream.ToArray();

// Deserialization
var obj = Serializer.Deserialize<Person>(new ReadOnlySpan<byte>(data));

Serialization APIs 🧩

Generic-constrained APIs 🔒

Serializer.Serialize<T>(...) and Serializer.Deserialize<T>(...) require that T implements IProtoParser<T> (i.e., a generated message type).

Note: These APIs are not supported in .netstandard2.0 due to lack of static virtual members in interfaces. Use IProtoParser-specified APIs instead.

IProtoParser-specified APIs 🧭

Serializer.Serialize<T>(..., IProtoWriter<T>) and Serializer.Deserialize<T>(..., IProtoReader<T>) where T is a [ProtoContract] marked type with generated IProtoParser<T> implementation.

Person person = new Person { Name = "Alice", Age = 30 };
ArrayBufferWriter<byte> bufferWriter = new ArrayBufferWriter<byte>();
LightProto.Serializer.Serialize<Person>(bufferWriter, person, Person.ProtoWriter); // must pass writer
var bytes = person.ToByteArray(Person.ProtoWriter); // extension method
Person result = LightProto.Serializer.Deserialize<Person>(bytes, Person.ProtoReader); // must pass reader

Dynamic APIs 🌀

Serializer.SerializeDynamically<T>(...) and Serializer.DeserializeDynamically<T>(...) resolve IProtoReader/Writer at runtime via T.ProtoReader/Writer or Serializer.RegisterParser or reflection.

Person person = new Person { Name = "Alice", Age = 30 };
ArrayBufferWriter<byte> bufferWriter = new ArrayBufferWriter<byte>();
LightProto.Serializer.SerializeDynamically<Person>(bufferWriter, person); // dynamic API
Person result = LightProto.Serializer.DeserializeDynamically<Person>(bufferWriter.WrittenSpan); // dynamic API

ProtoWriter/Reader resolution order:

  1. If a parser is registered via Serializer.RegisterParser<T>(reader, writer), use the registered parser.
  2. If T is a primitive/built-in type, use built-in parser from LightProto.Parser namespace which is registered internally.
  3. If T implements IProtoParser<T> (usually marked as [ProtoContract]), use T.ProtoWriter/Reader by reflection. This is fine with AOT, as the generic argument T is marked as [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)].
  4. If T is a generic container shape (e.g., List<>, Dictionary<,>, Nullable<>, etc.), try to resolve element type parser recursively. This may fail at runtime on AOT due to missing type metadata.

Non-generic APIs 🧱

Serializer.SerializeNonGeneric(..., object instance) and Serializer.DeserializeNonGeneric(Type type, ...) are similar to Dynamic APIs, but the type is specified at runtime.

Person person = new Person { Name = "Alice", Age = 30 };
ArrayBufferWriter<byte> bufferWriter = new ArrayBufferWriter<byte>();
LightProto.Serializer.SerializeNonGeneric(bufferWriter, person); // non-generic API
Person result = (Person)LightProto.Serializer.DeserializeNonGeneric(typeof(Person), bufferWriter.WrittenSpan); // non-generic API

The ProtoWriter/Reader resolution order is the same as Dynamic APIs.

.NET Standard 📦

In .NET Standard target frameworks, we can't use static virtual members in interface to find T.ProtoReader/Writer.

So LightProto requires you to specify a ProtoWriter when serializing and a ProtoReader when deserializing.

For [ProtoContract]-marked message types, ProtoReader/Writer is generated by LightProto, so use MessageType.ProtoReader/Writer.

For primitive types, LightProto provides predefined parsers in the LightProto.Parser namespace, such as LightProto.Parser.DateTimeParser.

If you don't need AOT support, you can use the dynamic APIs Serializer.SerializeDynamically<T> and Serializer.DeserializeDynamically<T> without passing ProtoReader/Writer.

Unity Support 🎮

LightProto's generated code supports C# 9, making it compatible with Unity projects targeting .NET Standard 2.0. You can use LightProto in Unity by following the same installation and usage instructions as for other .NET projects. IL2CPP builds are supported because LightProto is AOT-friendly.

Surrogates 🧬

protobuf-net can register surrogates via RuntimeTypeModel at runtime.

LightProto lets you specify a custom ProtoParserType for MessageType. For example, if MessageType is Person and the custom ProtoParserType is PersonProtoParser, you can use the following attributes in precedence order:

  1. member level: [ProtoMember(1,ParserType=typeof(PersonProtoParser))]
  2. class level: [ProtoParserTypeMap(typeof(Person), typeof(PersonProtoParser))]
  3. module/assembly level: [ProtoParserTypeMap(typeof(Person), typeof(PersonProtoParser))] (messageType and parserType should not be in the same assembly; if they are, use the type-level attribute.)
  4. type level: [ProtoParserTypeMap(typeof(Person), typeof(PersonProtoParser))]
  5. default: global::LightProto.Parser.PersonProtoParser

The ProtoParserType must implement IProtoParser<MessageType>. The easiest way is to define a SurrogateType with [ProtoContract] and mark it with [ProtoSurrogateFor<MessageType>]. Example for Person (can be any type):

[ProtoParserType(typeof(PersonProtoParser))] // type level ProtoParser
public class Person
{
 public string Name {get; set;}
 Person(){}
 public static Person FromName(string name) => new Person() { Name = name };
}

[ProtoContract]
[ProtoSurrogateFor<Person>] // mark this to tell source generator generate IProtoParser<Person> instead of `IProtoParser<PersonProtoParser>`
public partial struct PersonProtoParser
{
  [ProtoMember(1)]
  internal string Name { get; set; }    
  public static implicit operator Person(PersonProtoParser parser) // must define implicit conversions for surrogate type
  {
      return Person.FromName(parser.Name);
  }    
  public static implicit operator PersonProtoParser(Person value) // must define implicit conversions for surrogate type
  {
      return new PersonProtoParser() { Name = value.Name };
  }
}
[assembly: ProtoParserTypeMap(typeof(Person), typeof(PersonProtoParser))] // assembly level ProtoParser

[ProtoParserTypeMap(typeof(Person), typeof(PersonProtoParser))] // class level ProtoParser
public class MessageContract
{
  [ProtoMember(1,ParserType=typeof(PersonProtoParser))] //member level ProtoParser
  public Person Person {get; set;}      
}

You can also read/write raw binary data, but only WireType.LengthDelimited is supported for now because LightProtoGenerator needs to compute tags at compile time and any unknown type will be treated as LengthDelimited.

StringIntern 🧵

[StringIntern] attribute can be applied to individual string members, classes, modules, or assemblies.

RuntimeTypeModel 🧠

LightProto provides a set of APIs in RuntimeProtoWriter, RuntimeProtoReader, and RuntimeProtoParser.

RuntimeProtoParser<T> can be used to get IProtoReader<T> and IProtoWriter<T> at runtime.

You can use them to serialize/deserialize, or use Serializer.RegisterParser<T>(reader, writer) to register globally, then use Serializer.SerializeDynamically/DeserializeDynamically or Serializer.SerializeNonGeneric APIs.

public class TestMessage
{
    public int Value { get; set; }
    public string StringValue { get; set; } = string.Empty;
    public int[] IntArray { get; set; } = [];
}
var runtimeParser = new RuntimeProtoParser<TestMessage>(() => new());
runtimeParser.AddMember(1, message => message.Value, (message, value) => message.Value = value);
runtimeParser.AddMember(typeof(string), 2, message => message.StringValue, (message, value) => message.StringValue = (string)value);
runtimeParser.AddMember<int[]>(
    3,
    message => message.IntArray,
    (message, value) => message.IntArray = value,
    // specify array reader/writer for aot support
    Int32ProtoParser.ProtoReader.GetArrayReader(),
    Int32ProtoParser.ProtoWriter.GetCollectionWriter()
);

// Use the runtime parser to serialize/deserialize.
var writer = runtimeParser.ProtoWriter;
var reader = runtimeParser.ProtoReader;

If you do not need both Serialize and Deserialize, you can use RuntimeProtoWriter<T> or RuntimeProtoReader<T> to create only writer or reader.

public class TestMessage
{
    public int Value { get; set; }
    public string StringValue { get; set; } = string.Empty;
    public int[] IntArray { get; set; } = [];
}

var protoReader = new RuntimeProtoReader<TestMessage>(() => new());
protoReader.AddMember<int>(1, (message, value) => message.Value = value);
protoReader.AddMember(typeof(string), 2, (message, value) => message.StringValue = (string)value);
protoReader.AddMember<int[]>(3, (message, value) => message.IntArray = value, Int32ProtoParser.ProtoReader.GetArrayReader());

var protoWriter = new RuntimeProtoWriter<TestMessage>();
protoWriter.AddMember<int>(1, message => message.Value);
protoWriter.AddMember(typeof(string), 2, message => message.StringValue);
protoWriter.AddMember<int[]>(3, message => message.IntArray, Int32ProtoParser.ProtoWriter.GetCollectionWriter());

IExtensible 🧷

IExtensible is defined for compatibility only and has no effect.

Performance & Benchmarks 📊

The following benchmarks compare serialization performance between LightProto, protobuf-net, and Google.Protobuf.

You can reproduce these by cloning the repo and running tests/Benchmark.

BenchmarkDotNet v0.15.3, Windows 11 (10.0.26100.4652/24H2/2024Update/HudsonValley)
AMD Ryzen 7 5800X 3.80GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK 10.0.100-rc.1.25451.107
[Host]    : .NET 8.0.20 (8.0.20, 8.0.2025.41914), X64 RyuJIT x86-64-v3
.NET 10.0 : .NET 10.0.0 (10.0.0-rc.1.25451.107, 10.0.25.45207), X64 RyuJIT x86-64-v3
.NET 8.0  : .NET 8.0.20 (8.0.20, 8.0.2025.41914), X64 RyuJIT x86-64-v3
.NET 9.0  : .NET 9.0.9 (9.0.9, 9.0.925.41916), X64 RyuJIT x86-64-v3
Method Job Runtime Mean Error StdDev Ratio RatioSD Allocated Alloc Ratio
Serialize_ProtoBuf_net .NET 10.0 .NET 10.0 645.6 μs 12.70 μs 11.88 μs 1.39 0.03 526.41 KB 1.03
Serialize_GoogleProtoBuf .NET 10.0 .NET 10.0 539.9 μs 10.71 μs 12.75 μs 1.16 0.03 512.95 KB 1.00
Serialize_LightProto .NET 10.0 .NET 10.0 465.1 μs 7.88 μs 6.99 μs 1.00 0.02 512.95 KB 1.00
Serialize_ProtoBuf_net .NET 8.0 .NET 8.0 757.0 μs 12.80 μs 11.98 μs 1.42 0.04 526.41 KB 1.03
Serialize_GoogleProtoBuf .NET 8.0 .NET 8.0 553.9 μs 10.97 μs 9.72 μs 1.04 0.03 512.95 KB 1.00
Serialize_LightProto .NET 8.0 .NET 8.0 531.9 μs 10.52 μs 14.04 μs 1.00 0.04 512.95 KB 1.00
Serialize_ProtoBuf_net .NET 9.0 .NET 9.0 712.6 μs 13.61 μs 12.73 μs 1.39 0.04 526.41 KB 1.03
Serialize_GoogleProtoBuf .NET 9.0 .NET 9.0 546.7 μs 10.70 μs 16.33 μs 1.07 0.04 512.95 KB 1.00
Serialize_LightProto .NET 9.0 .NET 9.0 513.6 μs 10.15 μs 13.89 μs 1.00 0.04 512.95 KB 1.00
Method Job Runtime Mean Error StdDev Ratio RatioSD Allocated Alloc Ratio
Deserialize_ProtoBuf_net .NET 10.0 .NET 10.0 569.2 μs 10.88 μs 12.53 μs 1.38 0.04 562 KB 0.84
Deserialize_GoogleProtoBuf .NET 10.0 .NET 10.0 441.4 μs 8.67 μs 10.64 μs 1.07 0.04 648.7 KB 0.97
Deserialize_LightProto .NET 10.0 .NET 10.0 411.5 μs 8.08 μs 9.92 μs 1.00 0.03 665.95 KB 1.00
Deserialize_ProtoBuf_net .NET 8.0 .NET 8.0 688.0 μs 13.51 μs 15.56 μs 1.55 0.05 562 KB 0.84
Deserialize_GoogleProtoBuf .NET 8.0 .NET 8.0 595.5 μs 11.51 μs 16.14 μs 1.34 0.04 648.7 KB 0.97
Deserialize_LightProto .NET 8.0 .NET 8.0 444.8 μs 8.88 μs 9.12 μs 1.00 0.03 665.95 KB 1.00
Deserialize_ProtoBuf_net .NET 9.0 .NET 9.0 662.3 μs 12.60 μs 11.17 μs 1.53 0.04 562 KB 0.84
Deserialize_GoogleProtoBuf .NET 9.0 .NET 9.0 491.7 μs 9.64 μs 13.52 μs 1.14 0.04 648.7 KB 0.97
Deserialize_LightProto .NET 9.0 .NET 9.0 431.9 μs 8.33 μs 9.25 μs 1.00 0.03 665.95 KB 1.00

Note: Results vary by hardware, runtime, and data model. Please run the benchmarks on your environment for the most relevant numbers.

Working with .proto files 📄

LightProto doesn't ship a .proto → C# generator yet. You can generate C# using protobuf-net (or other tools), then adapt the output to LightProto (typically replacing the ProtoBuf namespace with LightProto and marking types partial). If something doesn't work, please file an issue.

If you need a dedicated .proto → C# generator, please vote on this issue.

Contributing 🤝

Contributions are welcome! Please see CONTRIBUTING for detailed contribution guidelines.

ARCHITECTURE.md describes the internal design and structure of LightProto, which may be helpful for contributors.

License 📄

MIT License — see LICENSE for details.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on LightProto:

Package Downloads
JK.Mexc.Net

JK.Mexc.Net is a high performance client library for accessing the Mexc REST and Websocket API. All data is mapped to readable models and enum values. Additional features include automatic websocket (re)connection management, an implementation for maintaining a client side order book, easy integration with other exchange client libraries and more.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.2.1 114 1/26/2026
1.2.0 100 1/21/2026
1.1.1 95 1/12/2026
1.1.0 96 1/10/2026
1.0.1 101 1/6/2026
1.0.0 97 1/4/2026
0.9.5 177 12/24/2025
0.9.4 272 12/18/2025
0.9.3 271 12/18/2025
0.9.2 277 12/17/2025
0.9.1 273 12/16/2025
0.9.0 5,152 12/12/2025
0.8.5 387 11/17/2025
0.8.4 227 10/27/2025
0.8.3 111 10/25/2025
0.8.2 126 10/25/2025
0.8.1 192 10/23/2025
0.8.0 196 10/20/2025
0.7.1 207 10/15/2025
0.7.0 117 10/11/2025
Loading failed