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
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:
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}" };
}
}
Now GET /v1/greet/Alice works alongside the gRPC endpoint.
Authentication
JWT bearer works as for HTTP. Pass via Metadata:
var metadata = new Metadata { { "Authorization", $"Bearer {token}" } };
await client.SayHelloAsync(req, metadata);
Deadlines
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
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 support — gRPC works in AOT.
- OpenTelemetry —
AddGrpcClientInstrumentation+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
✅ Correct: gRPC-Web
❌ Wrong: throw generic exceptions
✅ Correct: RpcException with appropriate status
❌ Wrong: no deadline
✅ Correct: deadline
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.