gRPC and Protocol Buffers

A contract-first binary RPC protocol over HTTP/2: where it beats JSON over HTTP between services, and where it does not.

Syntaxservice Greeter { rpc SayHello (HelloRequest) returns (HelloReply); }

gRPC is a way for one program to call a method inside another program over the network. The shape of every call is written down once, in a contract file. Tooling then generates the calling code for one side and a base class to fill in on the other, so neither side parses requests by hand. RPC stands for remote procedure call.

How a call travels

The contract is a .proto file. It names the service, its methods, and the messages they send and receive. Messages are encoded with Protocol Buffers, a binary format that identifies each field by a small number instead of by its name, so a message is smaller than the same data as JSON. The transport is HTTP/2, a version of HTTP that carries many calls at once over one connection. gRPC for ASP.NET Core checks that the request protocol is HTTP/2 and rejects anything else.

When to choose it

gRPC fits calls between services you control: high call volume, low latency, several languages sharing one contract, or long-lived streams of messages. Start the example server with dotnet run and call it with grpcurl, and greet.v1.Greeter/SayHello with {"name": "Ada"} answers {"message": "Hello Ada"}.

When not to

A browser cannot call a gRPC service directly, because browser JavaScript has no control over HTTP/2 frames; that needs gRPC-Web or JSON transcoding, which requires .NET 7 or later. The wire format is not readable, so inspecting traffic needs tooling that holds the contract. For pushing one message to many connected clients, SignalR fits better. For a public API with unknown callers, JSON over HTTP is still the safer default.

Example

Example · csharp
// Server project from: dotnet new grpc
// Protos/greet.proto, compiled on build by the Grpc.Tools package:
//     syntax = "proto3";
//     package greet.v1;
//     option csharp_namespace = "Greet.V1";
//     service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); }
//     message HelloRequest { string name = 1; }
//     message HelloReply { string message = 1; }

using Greet.V1;
using Grpc.Core;

var builder = WebApplication.CreateBuilder(args);

// Adds the gRPC server pieces to the app.
builder.Services.AddGrpc();

var app = builder.Build();

// Routes calls for greet.v1.Greeter to the class below.
app.MapGrpcService<GreeterService>();

app.Run();

// Greeter.GreeterBase is generated from the contract, not written by hand.
public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        return Task.FromResult(new HelloReply { Message = $"Hello {request.Name}" });
    }
}

When to use it

  • Choose between gRPC and a JSON HTTP API for a new internal service
  • Explain to a team why the browser cannot call the new gRPC endpoint directly
  • Add gRPC to an existing ASP.NET Core project rather than starting from the template
  • Serve gRPC from a container where an ingress terminates TLS

More examples

The project file behind the template

dotnet new grpc writes this for you, and this is what it wrote. The Protobuf item is what turns a .proto file into C#: without it the generated types do not exist and nothing compiles. GrpcServices takes Both (the default), Server, Client or None, so a server project asks for Server and a client project asks for Client. Reach for this when adding gRPC to a project that already exists.

Example · xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <!-- Grpc.AspNetCore brings in the server, Google.Protobuf and Grpc.Tools. -->
    <PackageReference Include="Grpc.AspNetCore" Version="2.71.0" />
  </ItemGroup>

  <ItemGroup>
    <!-- GrpcServices="Server" generates only the base class, not a client. -->
    <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
  </ItemGroup>

</Project>

Plaintext HTTP/2 for a container

TLS does more than encrypt: its ALPN handshake is how client and server agree on HTTP/2. An endpoint that serves both HTTP/1.1 and HTTP/2 cannot work without TLS, because every connection then defaults to HTTP/1.1. You reach for this when a sidecar or ingress terminates TLS and your container speaks plaintext behind it, which is the usual arrangement on Kubernetes.

Example · csharp
using Microsoft.AspNetCore.Server.Kestrel.Core;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();

builder.WebHost.ConfigureKestrel(options =>
{
    // No TLS inside the cluster, so there is no protocol negotiation.
    // Without Http2 here, every gRPC call fails: the connection would be HTTP/1.1.
    options.ListenAnyIP(8080, listen => listen.Protocols = HttpProtocols.Http2);
});

var app = builder.Build();
app.Run();

Calling the service from a console app

The same .proto file, compiled in a second project with GrpcServices="Client", produces a class with one method per rpc. There is no URL, no HTTP verb and no JSON in this code; the contract supplied all of it. This is the shape of almost every gRPC caller, and it is why sharing one .proto between repositories matters more than documenting the endpoint.

Example · csharp
using Greet.V1;
using Grpc.Net.Client;

// Client project needs Grpc.Net.Client, Google.Protobuf and Grpc.Tools,
// with <Protobuf Include="Protos\greet.proto" GrpcServices="Client" />.

using var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);

HelloReply reply = await client.SayHelloAsync(new HelloRequest { Name = "Ada" });
Console.WriteLine(reply.Message);

// Prints: Hello Ada

Discussion

  • Be the first to comment on this lesson.