.NET gRPC
gRPC is a contract-first RPC system: you declare services and messages in a .proto, code is generated from it on both ends, and calls travel as Protobuf over HTTP/2. Reach for it when the producer and consumer are both yours and you want a typed contract, low overhead, and streaming - internal service-to-service traffic above all. Stay on REST when the surface is a public or browser-facing API where ubiquity and human-readable bodies matter more; those endpoints are dotnet-minimal-api. This skill covers gRPC and nothing else. Baseline is .NET 8 / C# 12.
The .proto is the single source of truth
The contract lives in the .proto, not in C#. Define every service and message there and let codegen produce the C# types; treat the generated *.cs as build output you never open or edit.
Evolve the schema additively, never in place
Protobuf field numbers are the wire identity - the field name is irrelevant on the wire, the tag is everything. Compatibility is a discipline, not a feature you turn on:
- Add fields with brand-new tag numbers; a reader that does not know a field skips it.
- Never renumber a field, never reuse a retired tag, and never change a field's type. Retire a field with
reserved on its tag (and optionally its name) so nobody re-homes the number.
- Removing a field from the message is safe on the wire; reusing its number later is not.
- Prefer explicit-presence (
optional) on scalar fields when 'unset' must be distinguishable from the zero value - otherwise 0, "", and false are indistinguishable from absent.
- Enums must keep a zero member as the default/unknown value, and you add members, never reorder them.
Server
Host gRPC in ASP.NET Core via Grpc.AspNetCore:
builder.Services.AddGrpc();
// ...
app.MapGrpcService<OrdersService>();
- Keep the service class thin. It implements the generated base, translates the request message, and delegates to an application service; business logic does not live in the gRPC layer. Map the result back to a response message or throw an
RpcException (see status mapping below).
- gRPC needs HTTP/2 end to end. In
Development over Kestrel that works on plain HTTP; in production terminate with a proxy that speaks HTTP/2 to the backend (and keep ALPN intact). A request that arrives as HTTP/1.1 will fail the protocol check, not fall back.
- Reflection (
Grpc.AspNetCore.Server.Reflection) lets tools like grpcurl discover services - enable it in non-production only; it exposes your full schema.
- Tune limits deliberately:
MaxReceiveMessageSize / MaxSendMessageSize guard against oversized payloads (gRPC is for messages, not file transfer), and response compression (ResponseCompressionAlgorithm = "gzip") pays off on larger bodies - but do not compress a response that mixes a secret with attacker-influenced data, since compressing them together is a CRIME/BREACH-style oracle that leaks the secret by size.
Client
Register typed clients through DI so they ride IHttpClientFactory and a shared, correctly managed channel:
builder.Services
.AddGrpcClient<Orders.OrdersClient>(o => o.Address = new Uri("https://orders:5001"))
.AddStandardResilienceHandler();
AddGrpcClient<T> wires the generated client to a pooled HttpClient, so resilience (retries, timeouts, circuit breaker) layers on the same way it does for HTTP - the resilience configuration itself is dotnet-web-backend. Inject the client; do not construct it.
- The channel is the expensive, long-lived object and it multiplexes many concurrent calls over one HTTP/2 connection. Create it once and reuse it - per-request
GrpcChannel.ForAddress(...) is the classic gRPC performance bug. AddGrpcClient handles this for you; only hand-managed channels need the discipline spelled out.
- A unary call returns an awaitable plus access to response headers, trailers, and status via the call object when you need them.
Pick the call shape on purpose
Four shapes, one decision per method:
- Unary - one request, one response. The default; use it unless a stream earns its keep.
- Server streaming - one request, a stream of responses. Feeds, progress, paged or live result sets the client reads to completion.
- Client streaming - a stream of requests, one response. Uploads and batch ingestion where the server aggregates.
- Bidirectional streaming - independent request and response streams over one call. Live, conversational exchange; the two directions are not lock-step.
Non-negotiable on every call, streaming or not:
- Set a deadline (
CallOptions.Deadline / the deadline: argument). A call without one can hang indefinitely; the deadline is absolute (a point in time), it propagates to the server, and exceeding it surfaces as DeadlineExceeded.
- Honor the
CancellationToken end to end - pass the incoming token down through your awaits on the server, and stop enumerating a stream when the caller cancels. Streaming reads must also drain or cancel; an abandoned stream leaks the call.
Map domain outcomes to status codes
gRPC has its own status space; do not invent your own error envelope inside a successful response.
- Throw
RpcException with the right StatusCode for an expected failure - NotFound, InvalidArgument, AlreadyExists, PermissionDenied, Unauthenticated, FailedPrecondition. The whether-to-throw-or-return call at the language level is csharp; here, the wire signal for a failure is a status code, not a 200 carrying an error flag.
- Reserve
Internal (and Unknown) for genuine bugs - an unhandled exception maps there by default, and you must not leak its message or stack to the caller. Catch the expected cases and convert them to precise codes; let an interceptor handle the rest.
- Attach machine-readable detail with trailers / the rich error model when a status code alone is too coarse for the client to act on.
Cross-cutting concerns live in interceptors
Put logging, authentication checks, exception-to-status mapping, validation, and metrics in Interceptor subclasses (server and client side), not copied into every method.
- A server interceptor is the single place to log calls with their method and status, translate an unhandled exception into a clean
Internal/mapped status, and enforce request-level concerns - the gRPC analogue of middleware.
- A client interceptor is where you stamp outgoing metadata (auth tokens, correlation IDs) and observe call outcomes uniformly.
- Register server interceptors in
AddGrpc(o => o.Interceptors.Add<T>()); add client interceptors via .AddInterceptor<T>() on the client registration.
Auth
- Authenticate with a JWT bearer token carried in call metadata, or with mTLS (client certificates) for service-to-service trust - both are
dotnet-authentication. For JWT, attach the token from a client interceptor so no method has to remember it.
- Enforce on the server with
.RequireAuthorization() on the mapped service (or [Authorize] on the service class / methods), exactly as for HTTP endpoints. Authorization policies are the same machinery; gRPC just feeds them from metadata.
Health and observability
- Orchestrator probes speak the standard gRPC health-checking protocol - the opt-in wiring is in
references/optional-surfaces.md.
- gRPC integrates with the standard .NET observability stack; emit traces and metrics through it rather than bolting on a parallel logging path. Correlation and the broader telemetry setup are
dotnet-web-backend.
Browsers can't speak raw gRPC
A browser client needs gRPC-Web - the server and CORS wiring, and its streaming limits, are in references/optional-surfaces.md.
1---2name: dotnet-grpc3description: .NET gRPC conventions - the .proto is the contract and Grpc.Tools generates from it at build, host with Grpc.AspNetCore (AddGrpc + MapGrpcService), consume through typed clients (AddGrpcClient over IHttpClientFactory) with a reused multiplexing channel, the four call shapes with a deadline and CancellationToken on every one, JWT-bearer or mTLS auth, interceptors for cross-cutting work, the gRPC health protocol, and gRPC-Web for browsers. Floors at .NET 8 / C# 12. Load when defining, implementing, or calling a gRPC service, or weighing gRPC against REST. Companions: dotnet-web-backend, dotnet-authentication, csharp. Do NOT load for plain REST or minimal APIs - that is dotnet-minimal-api.4---56# .NET gRPC78gRPC is a contract-first RPC system: you declare services and messages in a `.proto`, code is generated from it on both ends, and calls travel as Protobuf over HTTP/2. Reach for it when the producer and consumer are both yours and you want a typed contract, low overhead, and streaming - internal service-to-service traffic above all. Stay on REST when the surface is a public or browser-facing API where ubiquity and human-readable bodies matter more; those endpoints are `dotnet-minimal-api`. This skill covers gRPC and nothing else. Baseline is .NET 8 / C# 12.910## The .proto is the single source of truth11The contract lives in the `.proto`, not in C#. Define every service and message there and let codegen produce the C# types; treat the generated `*.cs` as build output you never open or edit.1213- Generate with `Grpc.Tools` (a build-time, dev-only package). Reference each proto in the csproj and pick the side that project needs:14 ```xml15 <ItemGroup>16 <Protobuf Include="Protos\orders.proto" GrpcServices="Server" />17 </ItemGroup>18 ```19 Use `GrpcServices="Client"` in a consumer, `"Both"` only when a project is both. The server generates an abstract base to override; the client generates a strongly typed stub.20- Share the contract by sharing the `.proto`, not the compiled assembly: keep protos in one folder (or a dedicated contracts project / package) that both sides reference, so server and client always regenerate from the same file.2122### Evolve the schema additively, never in place23Protobuf field numbers are the wire identity - the field name is irrelevant on the wire, the tag is everything. Compatibility is a discipline, not a feature you turn on:24- Add fields with brand-new tag numbers; a reader that does not know a field skips it.25- Never renumber a field, never reuse a retired tag, and never change a field's type. Retire a field with `reserved` on its tag (and optionally its name) so nobody re-homes the number.26- Removing a field from the message is safe on the wire; reusing its number later is not.27- Prefer explicit-presence (`optional`) on scalar fields when 'unset' must be distinguishable from the zero value - otherwise `0`, `""`, and `false` are indistinguishable from absent.28- Enums must keep a zero member as the default/unknown value, and you add members, never reorder them.2930## Server31Host gRPC in ASP.NET Core via `Grpc.AspNetCore`:3233```csharp34builder.Services.AddGrpc();35// ...36app.MapGrpcService<OrdersService>();37```3839- Keep the service class thin. It implements the generated base, translates the request message, and delegates to an application service; business logic does not live in the gRPC layer. Map the result back to a response message or throw an `RpcException` (see status mapping below).40- gRPC needs HTTP/2 end to end. In `Development` over Kestrel that works on plain HTTP; in production terminate with a proxy that speaks HTTP/2 to the backend (and keep ALPN intact). A request that arrives as HTTP/1.1 will fail the protocol check, not fall back.41- Reflection (`Grpc.AspNetCore.Server.Reflection`) lets tools like `grpcurl` discover services - enable it in non-production only; it exposes your full schema.42- Tune limits deliberately: `MaxReceiveMessageSize` / `MaxSendMessageSize` guard against oversized payloads (gRPC is for messages, not file transfer), and response compression (`ResponseCompressionAlgorithm = "gzip"`) pays off on larger bodies - but do not compress a response that mixes a secret with attacker-influenced data, since compressing them together is a CRIME/BREACH-style oracle that leaks the secret by size.4344## Client45Register typed clients through DI so they ride `IHttpClientFactory` and a shared, correctly managed channel:4647```csharp48builder.Services49 .AddGrpcClient<Orders.OrdersClient>(o => o.Address = new Uri("https://orders:5001"))50 .AddStandardResilienceHandler();51```5253- `AddGrpcClient<T>` wires the generated client to a pooled `HttpClient`, so resilience (retries, timeouts, circuit breaker) layers on the same way it does for HTTP - the resilience configuration itself is `dotnet-web-backend`. Inject the client; do not construct it.54- The channel is the expensive, long-lived object and it multiplexes many concurrent calls over one HTTP/2 connection. Create it once and reuse it - per-request `GrpcChannel.ForAddress(...)` is the classic gRPC performance bug. `AddGrpcClient` handles this for you; only hand-managed channels need the discipline spelled out.55- A unary call returns an awaitable plus access to response headers, trailers, and status via the call object when you need them.5657## Pick the call shape on purpose58Four shapes, one decision per method:59- **Unary** - one request, one response. The default; use it unless a stream earns its keep.60- **Server streaming** - one request, a stream of responses. Feeds, progress, paged or live result sets the client reads to completion.61- **Client streaming** - a stream of requests, one response. Uploads and batch ingestion where the server aggregates.62- **Bidirectional streaming** - independent request and response streams over one call. Live, conversational exchange; the two directions are not lock-step.6364Non-negotiable on every call, streaming or not:65- Set a **deadline** (`CallOptions.Deadline` / the `deadline:` argument). A call without one can hang indefinitely; the deadline is absolute (a point in time), it propagates to the server, and exceeding it surfaces as `DeadlineExceeded`.66- Honor the `CancellationToken` end to end - pass the incoming token down through your awaits on the server, and stop enumerating a stream when the caller cancels. Streaming reads must also drain or cancel; an abandoned stream leaks the call.6768## Map domain outcomes to status codes69gRPC has its own status space; do not invent your own error envelope inside a successful response.70- Throw `RpcException` with the right `StatusCode` for an expected failure - `NotFound`, `InvalidArgument`, `AlreadyExists`, `PermissionDenied`, `Unauthenticated`, `FailedPrecondition`. The whether-to-throw-or-return call at the language level is `csharp`; here, the wire signal for a failure is a status code, not a 200 carrying an error flag.71- Reserve `Internal` (and `Unknown`) for genuine bugs - an unhandled exception maps there by default, and you must not leak its message or stack to the caller. Catch the expected cases and convert them to precise codes; let an interceptor handle the rest.72- Attach machine-readable detail with trailers / the rich error model when a status code alone is too coarse for the client to act on.7374## Cross-cutting concerns live in interceptors75Put logging, authentication checks, exception-to-status mapping, validation, and metrics in `Interceptor` subclasses (server and client side), not copied into every method.76- A **server interceptor** is the single place to log calls with their method and status, translate an unhandled exception into a clean `Internal`/mapped status, and enforce request-level concerns - the gRPC analogue of middleware.77- A **client interceptor** is where you stamp outgoing metadata (auth tokens, correlation IDs) and observe call outcomes uniformly.78- Register server interceptors in `AddGrpc(o => o.Interceptors.Add<T>())`; add client interceptors via `.AddInterceptor<T>()` on the client registration.7980## Auth81- Authenticate with a **JWT bearer** token carried in call metadata, or with **mTLS** (client certificates) for service-to-service trust - both are `dotnet-authentication`. For JWT, attach the token from a client interceptor so no method has to remember it.82- Enforce on the server with `.RequireAuthorization()` on the mapped service (or `[Authorize]` on the service class / methods), exactly as for HTTP endpoints. Authorization policies are the same machinery; gRPC just feeds them from metadata.8384## Health and observability85- Orchestrator probes speak the standard gRPC health-checking protocol - the opt-in wiring is in `references/optional-surfaces.md`.86- gRPC integrates with the standard .NET observability stack; emit traces and metrics through it rather than bolting on a parallel logging path. Correlation and the broader telemetry setup are `dotnet-web-backend`.8788## Browsers can't speak raw gRPC89A browser client needs **gRPC-Web** - the server and CORS wiring, and its streaming limits, are in `references/optional-surfaces.md`.