106. Case Study: Sharing Protobuf Files Between Go and C# Projects
106. Case Study: Sharing Protobuf Files Between Go and C# Projects
In the modern software era, interoperability between services has become absolutely crucial. One popular solution for cross-platform data exchange is Protobuf (Protocol Buffers). In this article, I’ll walk you through a concrete case study: sharing a Protobuf file between a Go backend project and a C#-based client application. We’ll cover the challenges, the solution, real code examples, and even a simulation and code flow illustrated with mermaid diagrams.
Background
Imagine the following scenario: you work on a backend team that develops the main service using Go (golang). Meanwhile, the client team builds a cross-platform desktop application based on C#. Both sides need to communicate using data that is structured, efficient, and type-safe. JSON is certainly flexible, but its serialization overhead is high and its type safety is limited. This is where Protobuf takes the stage.
Protobuf, the data serialization format created by Google, offers:
- Lightweight data transmission (binary and compressible)
- Backward compatibility via schema versioning
- Type safety across many languages, including Go and C#
- Solid tooling: code generators, validators, and so on.
However, aligning the Protobuf schema and the toolchain between two ecosystems isn’t always as simple as a copy-paste. Here’s the case study.
The Case: Sharing a Protobuf File Between Go & C#
Business Requirements
- The backend team (Go) builds a gRPC service.
- The client team (C#) consumes that gRPC and Protobuf service.
- Every schema change must stay in sync across both platforms.
- Both Go and C# developers must get typed code from the same .proto file.
The .proto File: A Simple Design
Suppose we have a user.proto schema:
1syntax = "proto3";
2
3package user;
4
5// A simple user record
6message User {
7 int32 id = 1;
8 string name = 2;
9 string email = 3;
10}
11
12// Request to fetch a user by ID
13message GetUserRequest {
14 int32 id = 1;
15}
16
17// User data response
18message GetUserResponse {
19 User user = 1;
20}
21
22// Service for users
23service UserService {
24 rpc GetUser(GetUserRequest) returns (GetUserResponse);
25}The Protobuf File-Sharing Workflow
Here’s the ideal flow for sharing a .proto schema between Go and C#:
graph TD
A[Developer Merancang user.proto] --> B[Commit ke Repo Protobuf]
B --> C[CI Build: Generate Kode Go]
B --> D[CI Build: Generate Kode C#]
C --> E[Backend Go Menggunakan hasil generate]
D --> F[Client C# Menggunakan hasil generate]
Notes:
- Protobuf Repo: A dedicated repo (or submodule) so the .proto file is always the single source of truth (SSOT).
- CI Build: Automated code generation via a pipeline (GitHub Actions or another CI).
Practical Steps: Integrating Protobuf Across Go & C#
Let’s look at the real process, along with the tooling involved.
1. Repo Structure for Collaboration
1/proto-schema/
2 └── user.proto
3/backend-go/
4 └── <Go code>
5/client-csharp/
6 └── <C# code>Set up proto-schema/ either as a submodule or within a mono-repo. This is the key to a single source so the schema doesn’t drift.
2. Generating Go Code
Install Protoc and the Go Plugins
1# Install the Protobuf Compiler
2brew install protobuf # (macOS)
3apt-get install protobuf-compiler # (Ubuntu)
4
5# Install the Go plugins
6go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
7go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latestGenerate the Go Files from .proto
1protoc --go_out=./backend-go --go-grpc_out=./backend-go \
2 --proto_path=./proto-schema \
3 ./proto-schema/user.protoResult: The Go files user.pb.go and user_grpc.pb.go are created in /backend-go/user/.
Snippet: Using It in Go
1import (
2 "context"
3 userpb "backend-go/user"
4)
5
6type UserServiceServer struct {
7 userpb.UnimplementedUserServiceServer
8}
9
10func (s *UserServiceServer) GetUser(ctx context.Context, req *userpb.GetUserRequest) (*userpb.GetUserResponse, error) {
11 user := &userpb.User{
12 Id: 42,
13 Name: "Alice",
14 Email: "alice@example.com",
15 }
16 return &userpb.GetUserResponse{User: user}, nil
17}3. Generating C# Code
Install Protoc + the C# Plugin
- Download protoc (https://github.com/protocolbuffers/protobuf/releases )
- Add the NuGet packages to the C# project:
1dotnet add package Grpc.Tools 2dotnet add package Google.Protobuf 3dotnet add package Grpc.Net.Client 4dotnet add package Grpc.AspNetCore
Generate the C# Files from .proto
1protoc --csharp_out=./client-csharp \
2 --grpc_out=./client-csharp \
3 --plugin=protoc-gen-grpc=packages/Grpc.Tools/tools/windows_x64/grpc_csharp_plugin.exe \
4 --proto_path=./proto-schema user.protoAutomatic Alternative (in .csproj):
Add this to the project file:
1<ItemGroup>
2 <Protobuf Include="..\proto-schema\user.proto" GrpcServices="Client" />
3</ItemGroup>Build the project, and the C# files are auto-generated (under obj/).
Snippet: Consuming gRPC in C#
1using Grpc.Net.Client;
2using User;
3
4var channel = GrpcChannel.ForAddress("https://localhost:5001");
5var client = new UserService.UserServiceClient(channel);
6
7var req = new GetUserRequest { Id = 42 };
8var resp = await client.GetUserAsync(req);
9
10Console.WriteLine($"User: {resp.User.Name}, {resp.User.Email}");Simulation: Schema Changes and Consistency
Let’s simulate a schema change and the propagation of that change to both platforms.
1. Adding a Field to User
1message User {
2 int32 id = 1;
3 string name = 2;
4 string email = 3;
5 string phone = 4; // New field!
6}Change Simulation Steps:
| Step | Backend Go | Client C# |
|---|---|---|
| Update .proto | Pull the proto-schema repo | Pull the proto-schema repo |
| Regenerate | protoc –go_out … | Build or protoc … |
| Use the field | user.Phone in Go | User.Phone in C# |
Result: Both sides immediately get user.Phone in a type-safe way.
Challenges and Tips
1. Compatibility:
- Follow the Protobuf rules (field numbers must not be changed; only add/remove optional fields).
- Avoid breaking changes (renaming a field only affects the code, not the wire format).
2. Tooling/Automation:
- Create a CI script/pipeline to generate and, if needed, publish the library.
- Pay attention to import paths between .proto files when there is more than one file.
3. Testing:
- Build integration tests that round-trip serialization/deserialization.
- Validate the service end-to-end (Go server ↔️ C# client).
Conclusion
Sharing a Protobuf schema between Go and C# projects is not merely a matter of generating files; it’s about the discipline of a single source of truth, automation, and best practices. The case study above has proven that, with the right tooling, you can manage a shared schema safely, without copy-paste, and without losing type safety.
I hope this article makes your next step easier as you build cross-platform applications, with Protobuf serving as a reliable bridge!
References:
Happy coding, and feel free to share your experiences or questions in the comments section!