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_namespace tells the compiler what namespace to apply to the generated C# classes.
  • Each field within HelloRequest and HelloReply is 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.Protobuf
  • Grpc.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.