gRPC and Protocol Buffer Service Contracts in C
gRPC is a high-performance, open-source Remote Procedure Call
framework developed by Google that enables efficient communication
between distributed systems using HTTP/2 and Protocol Buffers. This
article explains the fundamentals of gRPC, the role of Protocol Buffers
(Protobuf) as a contract-first interface definition language, and
provides a direct, step-by-step guide to defining .proto
files, compiling them, and implementing the resulting service contracts
within a modern C# (.NET) environment.
What is gRPC?
gRPC (Google Remote Procedure Call) allows a client application to directly call a method on a server application on a different machine as if it were a local object. It operates primarily over HTTP/2, providing built-in support for features such as multiplexing, bidirectional streaming, header compression, and strict transport security. Instead of using JSON or XML over standard REST endpoints, gRPC uses a binary serialization mechanism called Protocol Buffers, significantly reducing network payload sizes and CPU serialization overhead.
Understanding Protocol Buffers
Protocol Buffers (Protobuf) serve as both the Interface Definition
Language (IDL) and the binary serialization format for gRPC. In a
contract-first architecture, you define how data and service endpoints
look inside a .proto file before writing application
code.
Key components of a .proto file include:
- Syntax Declaration: Specifies the Protobuf version
(typically
proto3). - Packages and Namespaces: Prevents naming conflicts across projects and specifies language-specific targets.
- Messages: Data structures composed of strongly typed fields assigned unique numerical tags for binary encoding.
- Services: Interfaces defining RPC methods that accept and return specific messages.
Defining a Protocol Buffer Contract
To define a gRPC service contract, create a file named
greet.proto inside your project (typically placed in a
Protos folder). Below is a clear definition of a service
contract:
syntax = "proto3";
option csharp_namespace = "GrpcServiceDemo.Protos";
package greet;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply);
// Server-streaming example
rpc SayHelloStream (HelloRequest) returns (stream HelloReply);
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings.
message HelloReply {
string message = 1;
}In this contract:
option csharp_namespacetells the compiler what namespace to apply to the generated C# classes.- Each field within
HelloRequestandHelloReplyis assigned a unique field number (e.g.,= 1). These numbers identify fields in the binary message format and should never be altered once your API is in production.
Configuring the C# Project File
To compile .proto files into C# classes, add the
required tooling and references to your project. Install the following
NuGet packages:
Grpc.AspNetCore(for ASP.NET Core server implementations)Google.ProtobufGrpc.Tools
Next, configure the .csproj file to include the Protobuf
definition:
<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>Setting GrpcServices="Server" instructs the compiler to
generate server-side base classes. For client applications, this value
should be changed to "Client", or set to
"Both" if the project acts as an intermediary.
Implementing the Generated Service Contract in C
When the project builds, Grpc.Tools generates a base
class named Greeter.GreeterBase containing virtual methods
matching the RPC definitions in the .proto file.
You implement the contract by inheriting from this base class:
using Grpc.Core;
using GrpcServiceDemo.Protos;
namespace GrpcServiceDemo.Services;
public class GreeterService : Greeter.GreeterBase
{
private readonly ILogger<GreeterService> _logger;
public GreeterService(ILogger<GreeterService> logger)
{
_logger = logger;
}
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = $"Hello, {request.Name}!"
});
}
public override async Task SayHelloStream(
HelloRequest request,
IServerStreamWriter<HelloReply> responseStream,
ServerCallContext context)
{
for (int i = 0; i < 5; i++)
{
if (context.CancellationToken.IsCancellationRequested)
{
break;
}
await responseStream.WriteAsync(new HelloReply
{
Message = $"Hello, {request.Name}! Message #{i + 1}"
});
await Task.Delay(1000);
}
}
}Registering the Service in ASP.NET Core
To expose the service to incoming requests, register the gRPC
framework and map the service in Program.cs:
using GrpcServiceDemo.Services;
var builder = WebApplication.CreateBuilder(args);
// Register gRPC services in the dependency injection container
builder.Services.AddGrpc();
var app = builder.Build();
// Configure the HTTP request pipeline
app.MapGrpcService<GreeterService>();
app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client.");
app.Run();By decoupling the contract definition from the implementation language, gRPC ensures that any client—written in C#, Go, Python, or Java—can communicate with this .NET server seamlessly through strict, type-safe API boundaries.