This document explains how to contribute to LightProto: what you can change, what you should avoid, and how to run tests and benchmarks.
For architecture details, see ARCHITECTURE.md at the repository root.
- Understand the existing design first
- Read:
README.md,ARCHITECTURE.md, key files undersrc/LightProto/andsrc/LightProto.Generator/, and relevant tests undertests/.
- Read:
- Discuss non-trivial changes before implementing
- For new features or behavior changes, open an issue or discussion describing your proposal, and reference it from your PR.
- Keep PRs focused and minimal
- One PR should address one logical change (e.g. "add a new built-in parser", "fix encoding of a specific field", "add tests for feature X").
- Avoid large-scale reformatting, renaming, or unrelated refactors.
- Update or add tests
- Any behavior change must be covered by updated or new tests.
- Bug fixes should include a regression test demonstrating the fixed behavior.
- Run tests locally
- At minimum, run
dotnet test(see Section 4) and ensure relevant tests pass.
- At minimum, run
- Bug fixes
- Incorrect serialization/deserialization, unclear or wrong exceptions, edge cases, AOT-related issues, etc.
- New built-in parsers / type support
- Add support for common types in the
LightProto.Parsernamespace (and wire them into the generator mapping logic).
- Add support for common types in the
- Source generator behavior improvements
- Improve tag inference, collection/dictionary handling, inheritance and
ProtoInclude,ProtoParserTypeMapresolution, etc.
- Improve tag inference, collection/dictionary handling, inheritance and
- Performance optimizations
- Optimize hot paths in parsers,
Serializer,CodedInputStream/CodedOutputStream, etc., validated viatests/Benchmark.
- Optimize hot paths in parsers,
- Tests and documentation
- Add or improve unit and integration tests.
- Update documentation (
README.md,ARCHITECTURE.md,CONTRIBUTING.md, etc.) when directly related to your changes.
-
Do not introduce runtime reflection-based configuration paths
- LightProto is designed to be Native AOT friendly; serialization behavior must be driven by compile-time attributes and the source generator.
- Avoid designs that walk types with reflection at runtime to dynamically decide fields/tags.
-
Do not silently change existing wire format semantics
- Do not change how existing parsers encode the same type (e.g. ZigZag rules for
int, compatibility-level-specific encodings forGuid/decimal). - Do not change or reuse existing
[ProtoMember(tag)]numeric tags for different fields.
- Do not change how existing parsers encode the same type (e.g. ZigZag rules for
-
Be conservative with public API changes
- Public types in the
LightProtonamespace (e.g.Serializer,IProtoParser<T>, attributes) should not change signature or semantics without prior discussion. - If a breaking change is truly needed, mark it clearly in the PR description and describe the migration path.
- Public types in the
-
Do not remove existing tests or benchmarks casually
- Only remove tests when they are clearly invalid or tied to code that is legitimately being deleted or fundamentally reworked.
- The benchmark project (
tests/Benchmark) is a long-term performance reference and should not lose existing scenarios without strong justification.
-
Keep multi-targeting and AOT-related settings intact
- Do not remove existing
TargetFrameworks(e.g.netstandard2.0,net8.0,net9.0,net10.0) without broad consensus and clear justification. - Do not remove
<IsAotCompatible>true</IsAotCompatible>or similar AOT-related settings from project files.
- Do not remove existing
-
Avoid large-scale style-only refactors
- Do not reformat the entire codebase or enforce a new style across all files.
- Follow the "local style" rule: keep new code consistent with the style already used in the file.
For detailed architecture, see
ARCHITECTURE.md.
-
Runtime library:
src/LightProto/- Public API:
Serializer.*.cs,Attributes.cs,IProtoParser.cs,WireFormat.cs,CodedInputStream.cs,CodedOutputStream.cs, etc. - Built-in parsers:
src/LightProto/Parser/*.cs. - If your change is:
- Adding support for a primitive/collection type: implement/extend a parser under
Parser/and hook it up in the generator mapping. - Adjusting serialization behavior: change the relevant parser and/or
Serializerhelper methods.
- Adding support for a primitive/collection type: implement/extend a parser under
- Public API:
-
Source generator:
src/LightProto.Generator/- Entry point:
LightProtoGenerator.cs. - Core modeling logic: inner types like
ProtoContractandProtoMember, and methods such asGetProtoMembers/GetProtoParser. - If your change is:
- Supporting new attributes, implicit field rules, inheritance/interface scenarios, or parser resolution rules, this is the right place.
- Entry point:
-
Test projects:
tests/LightProto.Tests/– primary location for behavior and regression tests.tests/LightProto.AssemblyLevelTests/– tests for assembly-level[ProtoParserTypeMap]and cross-assembly behavior.tests/TestAot/– AOT sample; usually only adjusted for AOT-specific regressions.tests/Benchmark/– performance benchmarks.
-
The test framework is TUnit on top of the
Microsoft.Testingplatform. -
Typical style (see
tests/LightProto.Tests/SerializerTests.cs):namespace LightProto.Tests; public partial class SerializerTests { [Test] public async Task TestBufferWriter() { ArrayBufferWriter<byte> bufferWriter = new(); var obj = CreateTestContract(); Serializer.Serialize(bufferWriter, obj); var data = bufferWriter.WrittenSpan.ToArray(); var parsed = Serializer.Deserialize<TestContract>(data); await Assert.That(parsed.Name).IsEquivalentTo(obj.Name); } }
-
Common TUnit attributes:
[Test]– regular test.[Arguments(...)]– parameterized tests (seeDeserializeItemsand related tests).
From the repository root:
dotnet testThis builds and runs all test projects, including:
tests/LightProto.Teststests/LightProto.AssemblyLevelTests
To run just one test project:
cd tests/LightProto.Tests
dotnet testBenchmarks are mainly for evaluating performance impact; they are usually not required in CI but are recommended for perf-related changes.
From the benchmark project directory:
cd tests/Benchmark
dotnet run -c Release- This uses BenchmarkDotNet and will automatically run jobs for multiple target frameworks.
- To run a specific benchmark type or method, use BenchmarkDotNet filters (see its documentation).
tests/TestAot is a small net9.0 AOT console app using Google.Protobuf-generated types:
cd tests/TestAot
# Example publish command; adjust runtime identifier as needed
dotnet publish -c Release -r win-x64 -p:PublishAot=true- If you changed AOT-related code paths (e.g. reflection usage, trimming-sensitive APIs, AOT compatibility flags), it is recommended to ensure this project still publishes and runs.
- To format the codebase consistently, use the local dotnet tool:
dotnet tool run csharpier format . - To enable local pre-commit checks (if you have lefthook installed), install the repo hooks:
lefthook install
- You can use modern C# features (records, pattern matching,
usingdeclarations, etc.), but:- Keep style consistent with the surrounding file.
- Be mindful of target frameworks (especially the generator project which targets only
netstandard2.0).
- In the source generator, prefer using
LightProtoGeneratorExceptionto report structured diagnostics:- Give each diagnostic a stable Id (e.g.
LIGHT_PROTO_00X). - Provide actionable, clear messages that help users fix their code.
- Give each diagnostic a stable Id (e.g.
- In runtime code, avoid swallowing exceptions; use appropriate exception types such as
InvalidProtocolBufferExceptionorArgumentExceptionfor protocol and API misuse.
- Core
Serializerand parser paths are performance-sensitive:- Avoid unnecessary allocations (especially LINQ,
ToArray(), excessive string concatenation). - Prefer
Span<T>,ReadOnlySpan<T>,ReadOnlySequence<T>, andArrayPool<T>where appropriate.
- Avoid unnecessary allocations (especially LINQ,
- For performance-affecting changes, it is recommended to:
- Add or adjust benchmarks in
tests/Benchmark. - Include a brief before/after summary from BenchmarkDotNet output in the PR description.
- Add or adjust benchmarks in
Before opening a PR, consider checking the following:
- Scope is clear and focused (one PR does one thing).
- Code changes are placed in appropriate projects/files (runtime vs generator vs tests).
- Behavior changes are covered by updated or new tests.
- You have run locally at least:
-
dotnet tool run csharpier format .(or install lefthook pre-commit hooks to automate this). -
dotnet test(or the relevant individual test project).
-
- No existing public APIs were removed or renamed without clearly flagging a breaking change.
- Existing wire format semantics are unchanged, unless this is an intentional and well-discussed breaking change.
- For performance-related changes,
tests/Benchmarkhas been run and results are summarized in the PR (optional but recommended).
Thank you for contributing to LightProto!