Skip to content

gRPC

Key Points

  • gRPC = Google's RPC framework: HTTP/2 transport + Protobuf serialization. Strongly typed, binary, fast, polyglot.
  • Four call types: unary (1↔1), server-streaming (1→N), client-streaming (N→1), bidirectional (N↔N).
  • gRPC for .NET is first-class — built-in template, code-gen via Protobuf, integrated with DI, OTel, auth.
  • gRPC-Web for browser clients (browsers can't HTTP/2 trailers). JSON transcoding for REST-style clients.
  • vs REST: gRPC faster, smaller, typed contracts. REST: human-readable, browser-native, more familiar. gRPC for service-to-service; REST for public APIs.

Concepts (deep dive)

Protobuf contract

syntax = "proto3";
option csharp_namespace = "MyApp.Greet";

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

message HelloRequest { string name = 1; }
message HelloReply { string message = 1; }

.proto file is the source of truth. Code-gen produces server stubs and client classes.

Server

<PackageReference Include="Grpc.AspNetCore" Version="2.*" />
<ItemGroup>
  <Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>
public class GreeterService : Greeter.GreeterBase
{
    public override Task<HelloReply> SayHello(HelloRequest req, ServerCallContext ctx)
        => Task.FromResult(new HelloReply { Message = $"Hello {req.Name}" });
}

builder.Services.AddGrpc();
app.MapGrpcService<GreeterService>();

Client

<Protobuf Include="Protos\greet.proto" GrpcServices="Client" />
builder.Services.AddGrpcClient<Greeter.GreeterClient>(o =>
    o.Address = new Uri("https://grpc-server"))
    .AddStandardResilienceHandler();

public class C(Greeter.GreeterClient client)
{
    public async Task<string> Greet(string name)
    {
        var reply = await client.SayHelloAsync(new HelloRequest { Name = name });
        return reply.Message;
    }
}

Streaming

service Stocks {
    rpc Subscribe (StockRequest) returns (stream Quote);          // server stream
    rpc Upload (stream Tick) returns (Summary);                    // client stream
    rpc Trade (stream Order) returns (stream Confirmation);        // bi-directional
}
// Server streaming
public override async Task Subscribe(StockRequest req,
    IServerStreamWriter<Quote> stream, ServerCallContext ctx)
{
    while (!ctx.CancellationToken.IsCancellationRequested)
    {
        await stream.WriteAsync(new Quote { /* ... */ });
        await Task.Delay(1000, ctx.CancellationToken);
    }
}

// Client
using var call = client.Subscribe(new StockRequest { Symbol = "MSFT" });
await foreach (var quote in call.ResponseStream.ReadAllAsync())
    Console.WriteLine(quote.Price);

gRPC-Web

Browsers can't natively do gRPC (lacks HTTP/2 trailers from JS). gRPC-Web bridges:

builder.Services.AddGrpc();
app.UseGrpcWeb();
app.MapGrpcService<GreeterService>().EnableGrpcWeb();

Client (Blazor / JS):

var http = GrpcChannel.ForAddress("https://grpc-server", new GrpcChannelOptions
{
    HttpHandler = new GrpcWebHandler(new HttpClientHandler())
});
var client = new Greeter.GreeterClient(http);

JSON transcoding (.NET 7+)

Map gRPC service to REST-like HTTP/JSON:

import "google/api/annotations.proto";

service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply) {
        option (google.api.http) = { get: "/v1/greet/{name}" };
    }
}
builder.Services.AddGrpc().AddJsonTranscoding();

Now GET /v1/greet/Alice works alongside the gRPC endpoint.

Authentication

[Authorize]
public class GreeterService : Greeter.GreeterBase { /* ... */ }

JWT bearer works as for HTTP. Pass via Metadata:

var metadata = new Metadata { { "Authorization", $"Bearer {token}" } };
await client.SayHelloAsync(req, metadata);

Deadlines

var call = client.SayHelloAsync(req, deadline: DateTime.UtcNow.AddSeconds(5));

Server's ServerCallContext.CancellationToken fires at deadline.

Interceptors

public class LoggingInterceptor : Interceptor
{
    public override async Task<TResp> UnaryServerHandler<TReq, TResp>(
        TReq request, ServerCallContext ctx, UnaryServerMethod<TReq, TResp> continuation)
    {
        var sw = Stopwatch.StartNew();
        try { return await continuation(request, ctx); }
        finally { _log.LogInformation("{M} took {Ms}ms", ctx.Method, sw.ElapsedMilliseconds); }
    }
}

builder.Services.AddGrpc(o => o.Interceptors.Add<LoggingInterceptor>());

Cross-cutting middleware-equivalent for gRPC.

Status codes / errors

throw new RpcException(new Status(StatusCode.NotFound, "User missing"));
try { await client.SayHelloAsync(...); }
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound) { /* ... */ }

15 standard codes (OK, Cancelled, NotFound, etc.). Map domain errors carefully.

Performance

  • 5–10x smaller payloads than JSON (Protobuf binary).
  • HTTP/2 multiplexing — multiple calls per connection.
  • Streaming without polling.
  • Code-gen = no reflection at runtime.

For service-to-service, gRPC is faster than REST.

When gRPC vs REST

Scenario Choice
Service-to-service gRPC
Public API for many clients REST
Mobile (Android/iOS native) gRPC (rich client tooling)
Browser SPA REST or gRPC-Web
Streaming gRPC native
Strict typing gRPC (Protobuf)
Human-debuggable REST

When NOT gRPC

  • Browser-direct (without gRPC-Web).
  • Public API where consumers can't generate clients.
  • One-off / prototype code.

Schema evolution

  • Add fields: assigning new field number; old clients ignore.
  • Remove fields: don't reuse field numbers.
  • Renaming: ok if number unchanged.

.NET-specific features

  • Source-gen — compile-time stubs; AOT-friendly.
  • Native AOT supportgRPC works in AOT.
  • OpenTelemetryAddGrpcClientInstrumentation + AddAspNetCoreInstrumentation.

Code-first gRPC

Skip .proto files; define services in C# attributes:

[ServiceContract]
public interface IGreeter
{
    [OperationContract]
    ValueTask<HelloReply> SayHello(HelloRequest req);
}

Library: protobuf-net.Grpc. Convenient for .NET-only services; loses cross-language benefit.


Code: correct vs wrong

❌ Wrong: gRPC for browser without gRPC-Web

// Browser fetch can't use HTTP/2 trailers; gRPC fails

✅ Correct: gRPC-Web

app.UseGrpcWeb();
app.MapGrpcService<GreeterService>().EnableGrpcWeb();

❌ Wrong: throw generic exceptions

throw new InvalidOperationException("not found");   // becomes Internal status

✅ Correct: RpcException with appropriate status

throw new RpcException(new Status(StatusCode.NotFound, "User missing"));

❌ Wrong: no deadline

await client.SayHelloAsync(req);   // can hang indefinitely

✅ Correct: deadline

await client.SayHelloAsync(req, deadline: DateTime.UtcNow.AddSeconds(5));

Design patterns for this topic

Pattern 1 — "gRPC for service-to-service"

  • Intent: typed, fast, internal.

Pattern 2 — "JSON transcoding for hybrid"

  • Intent: one definition; both gRPC + REST clients.

Pattern 3 — "Interceptors for cross-cutting"

  • Intent: logging, auth, retry.

Pattern 4 — "Deadlines on every call"

  • Intent: abandon doomed requests.

Pattern 5 — "Streaming for telemetry / push"

  • Intent: native push without polling.

Pros & cons / trade-offs

Aspect Pros Cons
gRPC Fast; typed Browser limits; less debuggable
REST Human-readable; browser-native Slower; JSON-bloated
gRPC-Web Browser support Needs proxy
JSON transcoding One def; two clients Build complexity

When to use / when to avoid

  • Use gRPC for internal service-to-service.
  • Use for streaming workloads.
  • Avoid for public REST-style APIs.
  • Avoid browser-direct without gRPC-Web.

Interview Q&A

Q1. What is gRPC? RPC over HTTP/2 + Protobuf. Strongly typed; fast; polyglot.

Q2. Four call types? Unary, server-streaming, client-streaming, bidirectional.

Q3. Why HTTP/2? Multiplexing (many calls per connection), header compression, streaming.

Q4. Why Protobuf? Binary; smaller than JSON; strict schema; backwards-compatible.

Q5. Why can't browsers do gRPC directly? HTTP/2 trailers required by gRPC; not exposed by browser fetch APIs. gRPC-Web bridges.

Q6. JSON transcoding? Map a gRPC service to REST-style HTTP/JSON. One service definition; two protocols.

Q7. Deadlines? Per-call timeout. Server's CancellationToken fires when exceeded.

Q8. RpcException? Standard error mechanism. Status code + message. 15 standard codes.

Q9. Field number evolution? Add new numbers freely. Don't reuse old ones (deprecate, don't repurpose).

Q10. Code-first vs proto-first? Proto-first: cross-language; standard. Code-first: .NET-only convenience via protobuf-net.

Q11. gRPC vs SignalR? gRPC: typed RPC + streaming. SignalR: bidirectional pub/sub for clients (browsers).

Q12. AOT support? Yes. gRPC for .NET works under Native AOT.


Gotchas / common mistakes

  • ⚠️ Browser without gRPC-Web — fails.
  • ⚠️ No deadline — hangs.
  • ⚠️ Generic exceptions — wrong status code.
  • ⚠️ Reused field numbers — silent corruption.
  • ⚠️ Missing OpenTelemetry instrumentation.

Further reading