How to Use Protocol Buffers in C#

A practical guide to implementing Protocol Buffers in your C# applications

Published: January 2025 • 8 min read

Protocol Buffers (protobuf) works seamlessly with C# and .NET applications. This guide walks you through everything you need to know to start using protobuf in your C# projects, from installation to practical implementation.

We'll build a simple example that demonstrates how to define messages, generate C# code, and serialize/deserialize data. By the end of this guide, you'll be able to integrate protobuf into your own applications.

Prerequisites

Before we start, make sure you have:

  • .NET SDK: .NET 6.0 or later (download from dotnet.microsoft.com)
  • Visual Studio or VS Code: Any C# editor you're comfortable with
  • Basic C# knowledge: Understanding of classes and objects

Step 1: Create a New C# Project

First, create a new console application:

dotnet new console -n ProtobufExample
cd ProtobufExample

This creates a basic C# console application where we'll add Protocol Buffers support.

Step 2: Install Google.Protobuf NuGet Package

Add the Google.Protobuf package to your project:

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

Note: Google.Protobuf provides runtime support for protobuf in C#, while Grpc.Tools includes the protoc compiler for generating C# code from .proto files.

Step 3: Define Your .proto Schema

Create a new folder called Protos in your project root, then create a file named person.proto:

syntax = "proto3";

package Example;

message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;
  repeated string phone_numbers = 4;
  
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  
  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }
  
  repeated PhoneNumber phones = 5;
}

message AddressBook {
  repeated Person people = 1;
}

This schema defines two message types:

  • Person: Contains basic contact information including name, ID, email, and phone numbers
  • AddressBook: A collection of Person objects

Step 4: Configure Your Project File

Edit your ProtobufExample.csproj file to include the .proto file:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.25.0" />
    <PackageReference Include="Grpc.Tools" Version="2.59.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>

  <ItemGroup>
    <Protobuf Include="Protos\person.proto" GrpcServices="None" />
  </ItemGroup>

</Project>

The <Protobuf Include> element tells the build system to automatically generate C# code from your .proto file.

Step 5: Build the Project

Build your project to generate the C# classes:

dotnet build

This generates C# classes in the obj folder. You can now use the Person andAddressBook classes in your code.

Step 6: Use Protocol Buffers in Your Code

Now let's use the generated classes. Replace the contents of Program.cswith:

using System;
using System.IO;
using Example;

class Program
{
    static void Main(string[] args)
    {
        // Create a new Person object
        var person = new Person
        {
            Name = "John Doe",
            Id = 1234,
            Email = "john.doe@example.com"
        };

        // Add phone numbers
        person.PhoneNumbers.Add("555-1234");
        person.PhoneNumbers.Add("555-5678");

        // Add a structured phone number
        person.Phones.Add(new Person.Types.PhoneNumber
        {
            Number = "555-9999",
            Type = Person.Types.PhoneType.Mobile
        });

        Console.WriteLine("Created Person:");
        Console.WriteLine($"Name: {person.Name}");
        Console.WriteLine($"ID: {person.Id}");
        Console.WriteLine($"Email: {person.Email}");

        // Serialize to binary
        byte[] binaryData;
        using (var stream = new MemoryStream())
        {
            person.WriteTo(stream);
            binaryData = stream.ToArray();
        }

        Console.WriteLine($"\nSerialized to {binaryData.Length} bytes");

        // Deserialize from binary
        Person deserializedPerson;
        using (var stream = new MemoryStream(binaryData))
        {
            deserializedPerson = Person.Parser.ParseFrom(stream);
        }

        Console.WriteLine("\nDeserialized Person:");
        Console.WriteLine($"Name: {deserializedPerson.Name}");
        Console.WriteLine($"ID: {deserializedPerson.Id}");
        Console.WriteLine($"Email: {deserializedPerson.Email}");
        Console.WriteLine($"Phone numbers: {string.Join(", ", deserializedPerson.PhoneNumbers)}");

        // Save to file
        using (var output = File.Create("person.bin"))
        {
            person.WriteTo(output);
        }
        Console.WriteLine("\nSaved to person.bin");

        // Read from file
        using (var input = File.OpenRead("person.bin"))
        {
            var personFromFile = Person.Parser.ParseFrom(input);
            Console.WriteLine($"\nRead from file: {personFromFile.Name}");
        }
    }
}

Step 7: Run Your Application

Run the application:

dotnet run

You should see output like:

Created Person:
Name: John Doe
ID: 1234
Email: john.doe@example.com

Serialized to 62 bytes

Deserialized Person:
Name: John Doe
ID: 1234
Email: john.doe@example.com
Phone numbers: 555-1234, 555-5678

Saved to person.bin

Read from file: John Doe

Common Operations

Serialize to Byte Array

byte[] data = person.ToByteArray();

Deserialize from Byte Array

Person person = Person.Parser.ParseFrom(data);

Convert to JSON

using Google.Protobuf;

string json = JsonFormatter.Default.Format(person);
Console.WriteLine(json);

Parse from JSON

string json = "{\"name\": \"Jane\", \"id\": 5678}";
Person person = JsonParser.Default.Parse<Person>(json);

Clone a Message

Person copy = person.Clone();

Merge Two Messages

person1.MergeFrom(person2); // Merges person2 into person1

Best Practices for C# Protobuf

Use Proper Naming Conventions

In .proto files, use snake_case (e.g., phone_number). The C# generator automatically converts these to PascalCase (e.g., PhoneNumber).

Handle Repeated Fields as Collections

Repeated fields in protobuf become RepeatedField<T> in C#. You can use them like regular lists with Add(), Clear(), and foreach.

Use Binary for Network/Storage, JSON for Debugging

Serialize to binary format for production (smaller, faster). Use JSON format for debugging and logging since it's human-readable.

Dispose Streams Properly

Always use using statements when working with streams to ensure proper resource cleanup.

Common Issues and Solutions

Issue: Generated classes not found

Solution: Run dotnet build to generate the classes. Make sure the .proto file is included in your .csproj file.

Issue: Package namespace not found

Solution: Check the package declaration in your .proto file. Use that namespace in your C# code (e.g., using Example;).

Issue: Serialization fails silently

Solution: Check that all required fields are set. In proto3, all fields are optional, but you may have validation logic that requires certain fields.

Related Tools

Conclusion

Protocol Buffers integrates smoothly with C# and .NET applications. The Google.Protobuf library provides a clean API that feels natural to C# developers, with strong typing and good performance.

Start with simple examples like the one in this guide, then gradually adopt protobuf for performance-critical parts of your application. The initial setup takes a bit of work, but the benefits in terms of speed, size, and type safety make it worthwhile for production systems.