Protobuf Serialization in .NET with Google.Protobuf and gRPC

Protocol Buffers (Protobuf) is Google's language-neutral, schema-driven binary serialization format. In the .NET ecosystem, it's most commonly encountered as the wire format for gRPC, but it's equally useful for any scenario where you need compact, versioned, cross-platform serialization.

Schema-first design

Unlike JSON serialization where you start with C# classes, Protobuf starts with a .proto schema file:

protobuf
syntax = "proto3";

option csharp_namespace = "MyApp.Contracts";

message Order {
  int32 id = 1;
  string customer_name = 2;
  repeated LineItem items = 3;
  google.protobuf.Timestamp created_at = 4;
}

message LineItem {
  string product = 1;
  int32 quantity = 2;
  double unit_price = 3;
}

Each field has a numeric tag (the = 1, = 2, etc.) that identifies it in the binary format. These tags — not the field names — are what matter on the wire, which makes renaming fields a non-breaking change.

Setting up in .NET

Install the required packages:

dotnet add package Google.Protobuf
dotnet add package Grpc.Tools

Add your .proto files to the project:

config.xml
<ItemGroup>
    <Protobuf Include="Protos\order.proto" GrpcServices="None" />
</ItemGroup>

Set GrpcServices="None" if you only want the message classes. Use "Server", "Client", or "Both" if you're also defining gRPC services.

The Grpc.Tools package includes the Protobuf compiler (protoc), which runs at build time and generates C# classes from your .proto files.

Using generated classes

The generated classes are fully functional C# objects:

Example.cs
var order = new Order
{
    Id = 1,
    CustomerName = "Alice",
    CreatedAt = Timestamp.FromDateTime(DateTime.UtcNow),
    Items =
    {
        new LineItem { Product = "Widget", Quantity = 3, UnitPrice = 9.99 },
        new LineItem { Product = "Gadget", Quantity = 1, UnitPrice = 24.50 }
    }
};

Serialize to bytes and back:

Example.cs
// Serialize
byte[] bytes = order.ToByteArray();

// Deserialize
var restored = Order.Parser.ParseFrom(bytes);

You can also serialize to and from streams, which is more efficient for large messages:

Example.cs
using var stream = new MemoryStream();
order.WriteTo(stream);

stream.Position = 0;
var restored = Order.Parser.ParseFrom(stream);

Using with gRPC

Define a service in your .proto file:

protobuf
service OrderService {
  rpc GetOrder (GetOrderRequest) returns (Order);
  rpc ListOrders (ListOrdersRequest) returns (stream Order);
}

message GetOrderRequest {
  int32 id = 1;
}

message ListOrdersRequest {
  int32 page_size = 1;
  string page_token = 2;
}

Implement the server:

Example.cs
public class OrderServiceImpl : OrderService.OrderServiceBase
{
    public override async Task<Order> GetOrder(
        GetOrderRequest request, ServerCallContext context)
    {
        var order = await _repository.FindAsync(request.Id);
        return MapToProto(order);
    }
}

The gRPC tooling generates both the server base class and a typed client, giving you end-to-end type safety from a single schema definition.

Schema evolution rules

Protobuf is designed for forward and backward compatibility, provided you follow these rules:

protobuf
message Order {
  reserved 5;         // was 'discount'
  reserved "discount";
  int32 id = 1;
  // ...
}

Protobuf vs MessagePack vs JSON

Aspect Protobuf MessagePack JSON
Schema Required (.proto) Optional (attributes) None
Human-readable No No Yes
Cross-language Excellent Good Excellent
Payload size Smallest Small Largest
.NET integration Good (gRPC) Good (SignalR) Native
Versioning Built-in rules Manual Manual

Choose Protobuf when you need strict schema contracts across teams or languages, particularly for gRPC services. Choose MessagePack for .NET-to-.NET scenarios where you want binary performance without a schema file. Choose JSON for public APIs and simplicity.

Wrapping up

Protobuf brings rigour to serialization. The schema-first approach forces you to think about versioning, field types, and contracts before writing application code. In a microservices architecture where multiple teams consume your data, that discipline pays dividends. Start with gRPC — it's the most natural entry point — and consider Protobuf for caching, messaging, and event storage once you're comfortable with the tooling.