--- name: dotnet-grpc description: "Building gRPC services. Proto definition, code-gen, ASP.NET Core host, streaming, auth." --- # dotnet-grpc Full gRPC lifecycle for .NET applications. Covers `.proto` service definition, code generation, ASP.NET Core gRPC server implementation and endpoint hosting, `Grpc.Net.Client` client patterns, all four streaming patterns (unary, server streaming, client streaming, bidirectional streaming), authentication, load balancing, and health checks. **Out of scope:** Source generator authoring patterns (incremental generator API, Roslyn syntax trees) -- see [skill:dotnet-csharp-source-generators]. HTTP client factory patterns and resilience pipeline configuration -- see [skill:dotnet-http-client] and [skill:dotnet-resilience]. Native AOT architecture and trimming strategies -- see [skill:dotnet-native-aot] for AOT compilation, [skill:dotnet-aot-architecture] for AOT-first design patterns, and [skill:dotnet-trimming] for trim-safe development. Cross-references: [skill:dotnet-resilience] for retry/circuit-breaker on gRPC channels, [skill:dotnet-serialization] for Protobuf wire format details. See [skill:dotnet-integration-testing] for testing gRPC services. --- ## Proto Definition and Code Generation ### Project Setup gRPC uses Protocol Buffers as its interface definition language. The `Grpc.Tools` package generates C# code from `.proto` files at build time. **Server project:** ```xml ``` **Client project:** ```xml ``` **Shared contracts project (recommended for larger services):** ```xml ``` ### Proto File Definition ```protobuf syntax = "proto3"; option csharp_namespace = "MyApp.Grpc"; package myapp; import "google/protobuf/timestamp.proto"; import "google/protobuf/empty.proto"; // Service definition with all 4 streaming patterns service OrderService { // Unary: single request, single response rpc GetOrder (GetOrderRequest) returns (OrderResponse); // Server streaming: single request, stream of responses rpc ListOrders (ListOrdersRequest) returns (stream OrderResponse); // Client streaming: stream of requests, single response rpc UploadOrders (stream CreateOrderRequest) returns (UploadOrdersResponse); // Bidirectional streaming: stream of requests, stream of responses rpc ProcessOrders (stream CreateOrderRequest) returns (stream OrderResponse); } message GetOrderRequest { int32 id = 1; } message ListOrdersRequest { string customer_id = 1; int32 page_size = 2; string page_token = 3; } message CreateOrderRequest { string customer_id = 1; repeated OrderItemMessage items = 2; } message OrderResponse { int32 id = 1; string customer_id = 2; repeated OrderItemMessage items = 3; google.protobuf.Timestamp created_at = 4; } message OrderItemMessage { string product_id = 1; int32 quantity = 2; double unit_price = 3; } message UploadOrdersResponse { int32 orders_created = 1; } ``` ### Code-Gen Workflow The `Grpc.Tools` package runs the Protobuf compiler (`protoc`) and C# gRPC plugin at build time. Generated files appear in `obj/` and are included automatically: 1. Add `.proto` files to the project via `` items 2. Set `GrpcServices` to `Server`, `Client`, or `Both` 3. Build the project -- generated C# types and service stubs appear in `obj/Debug/net10.0/Protos/` 4. Implement the generated abstract base class (server) or use the generated client class The gRPC code-gen toolchain uses source generation to produce the C# stubs from `.proto` definitions. This is conceptually similar to [skill:dotnet-csharp-source-generators] but uses `protoc` rather than Roslyn incremental generators. --- ## ASP.NET Core gRPC Server ### Service Implementation Implement the generated abstract base class: ```csharp using Grpc.Core; using MyApp.Grpc; public sealed class OrderGrpcService( OrderRepository repository, ILogger logger) : OrderService.OrderServiceBase { // Unary public override async Task GetOrder( GetOrderRequest request, ServerCallContext context) { var order = await repository.GetByIdAsync(request.Id, context.CancellationToken); if (order is null) { throw new RpcException(new Status(StatusCode.NotFound, $"Order {request.Id} not found")); } return MapToResponse(order); } // Server streaming public override async Task ListOrders( ListOrdersRequest request, IServerStreamWriter responseStream, ServerCallContext context) { await foreach (var order in repository.ListByCustomerAsync( request.CustomerId, context.CancellationToken)) { await responseStream.WriteAsync(MapToResponse(order), context.CancellationToken); } } // Client streaming public override async Task UploadOrders( IAsyncStreamReader requestStream, ServerCallContext context) { var count = 0; await foreach (var request in requestStream.ReadAllAsync( context.CancellationToken)) { await repository.CreateAsync(MapFromRequest(request), context.CancellationToken); count++; } return new UploadOrdersResponse { OrdersCreated = count }; } // Bidirectional streaming public override async Task ProcessOrders( IAsyncStreamReader requestStream, IServerStreamWriter responseStream, ServerCallContext context) { await foreach (var request in requestStream.ReadAllAsync( context.CancellationToken)) { var order = await repository.CreateAsync(MapFromRequest(request), context.CancellationToken); await responseStream.WriteAsync(MapToResponse(order), context.CancellationToken); } } private static OrderResponse MapToResponse(Order order) => new() { Id = order.Id, CustomerId = order.CustomerId, CreatedAt = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTimeOffset( order.CreatedAt) }; private static Order MapFromRequest(CreateOrderRequest request) => new() { CustomerId = request.CustomerId, Items = request.Items.Select(i => new OrderItem { ProductId = i.ProductId, Quantity = i.Quantity, UnitPrice = (decimal)i.UnitPrice }).ToList() }; } ``` ### Endpoint Hosting Register gRPC services in the ASP.NET Core pipeline: ```csharp var builder = WebApplication.CreateBuilder(args); // Add gRPC services builder.Services.AddGrpc(options => { options.MaxReceiveMessageSize = 4 * 1024 * 1024; // 4 MB options.MaxSendMessageSize = 4 * 1024 * 1024; options.EnableDetailedErrors = builder.Environment.IsDevelopment(); }); var app = builder.Build(); // Map gRPC service endpoints app.MapGrpcService(); app.Run(); ``` ### gRPC Reflection (Development) Enable gRPC reflection for tools like `grpcurl` and `grpcui`: ```csharp builder.Services.AddGrpc(); builder.Services.AddGrpcReflection(); var app = builder.Build(); app.MapGrpcService(); if (app.Environment.IsDevelopment()) { app.MapGrpcReflectionService(); } ``` --- ## Client Patterns with Grpc.Net.Client ### Basic Client ```csharp using Grpc.Net.Client; using MyApp.Grpc; // Create a channel (reuse across calls -- channels are expensive to create) using var channel = GrpcChannel.ForAddress("https://localhost:5001"); var client = new OrderService.OrderServiceClient(channel); // Unary call var response = await client.GetOrderAsync( new GetOrderRequest { Id = 42 }); ``` ### DI-Registered Client with IHttpClientFactory Register gRPC clients via `IHttpClientFactory` for connection pooling and resilience: ```csharp builder.Services .AddGrpcClient(options => { options.Address = new Uri("https://order-service:5001"); }) .ConfigureChannel(options => { options.MaxReceiveMessageSize = 4 * 1024 * 1024; }); ``` Apply resilience via [skill:dotnet-resilience]: ```csharp builder.Services .AddGrpcClient(options => { options.Address = new Uri("https://order-service:5001"); }) .AddStandardResilienceHandler(); ``` ### Reading Server Streams ```csharp using var call = client.ListOrders( new ListOrdersRequest { CustomerId = "cust-123" }); await foreach (var order in call.ResponseStream.ReadAllAsync()) { Console.WriteLine($"Order {order.Id}: {order.CustomerId}"); } ``` ### Client Streaming ```csharp using var call = client.UploadOrders(); foreach (var order in ordersToCreate) { await call.RequestStream.WriteAsync(new CreateOrderRequest { CustomerId = order.CustomerId }); } // Signal completion await call.RequestStream.CompleteAsync(); // Read the response var response = await call; Console.WriteLine($"Created {response.OrdersCreated} orders"); ``` ### Bidirectional Streaming ```csharp using var call = client.ProcessOrders(); // Start reading responses in background var readTask = Task.Run(async () => { await foreach (var response in call.ResponseStream.ReadAllAsync()) { Console.WriteLine($"Processed order {response.Id}"); } }); // Send requests foreach (var order in ordersToProcess) { await call.RequestStream.WriteAsync(new CreateOrderRequest { CustomerId = order.CustomerId }); } await call.RequestStream.CompleteAsync(); await readTask; ``` --- ## Streaming Patterns Summary gRPC supports four communication patterns: | Pattern | Request | Response | Use Case | |---------|---------|----------|----------| | **Unary** | Single message | Single message | Standard request-response (CRUD, queries) | | **Server streaming** | Single message | Stream of messages | Real-time feeds, large result sets, push notifications | | **Client streaming** | Stream of messages | Single message | Bulk uploads, aggregation, telemetry ingestion | | **Bidirectional streaming** | Stream of messages | Stream of messages | Chat, real-time collaboration, event processing | --- ## Authentication ### Bearer Token (JWT) Server-side authentication: ```csharp builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = "https://identity.example.com"; options.TokenValidationParameters.ValidAudience = "order-api"; }); builder.Services.AddAuthorization(); builder.Services.AddGrpc(); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapGrpcService().RequireAuthorization(); ``` Client-side token propagation: ```csharp builder.Services .AddGrpcClient(options => { options.Address = new Uri("https://order-service:5001"); }) .AddCallCredentials(async (context, metadata, serviceProvider) => { var tokenProvider = serviceProvider.GetRequiredService(); var token = await tokenProvider.GetTokenAsync(context.CancellationToken); metadata.Add("Authorization", $"Bearer {token}"); }); ``` ### Certificate Authentication (mTLS) For service-to-service authentication with mutual TLS: ```csharp // Server: require client certificates builder.WebHost.ConfigureKestrel(kestrel => { kestrel.ConfigureHttpsDefaults(https => { https.ClientCertificateMode = ClientCertificateMode.RequireCertificate; }); }); builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme) .AddCertificate(options => { options.AllowedCertificateTypes = CertificateTypes.Chained; options.RevocationMode = X509RevocationMode.NoCheck; // Configure per environment }); ``` ```csharp // Client: provide client certificate var handler = new HttpClientHandler(); handler.ClientCertificates.Add( new X509Certificate2("client.pfx", "password")); using var channel = GrpcChannel.ForAddress("https://order-service:5001", new GrpcChannelOptions { HttpHandler = handler }); ``` --- ## Load Balancing ### Client-Side Load Balancing gRPC supports client-side load balancing with service discovery: ```csharp // DNS-based service discovery with round-robin builder.Services .AddGrpcClient(options => { options.Address = new Uri("dns:///order-service:5001"); }) .ConfigureChannel(options => { options.Credentials = ChannelCredentials.Insecure; options.ServiceConfig = new ServiceConfig { LoadBalancingConfigs = { new RoundRobinConfig() } }; }); ``` ### Proxy-Based Load Balancing For environments with a load balancer (e.g., Kubernetes, Envoy, YARP): - Use **L7 (HTTP/2-aware) load balancers** -- L4 load balancers route at the TCP level and pin all gRPC requests to a single backend because HTTP/2 multiplexes on a single connection. - Envoy, Linkerd, and Kubernetes ingress controllers with gRPC support distribute requests at the RPC level. - Configure `SocketsHttpHandler.EnableMultipleHttp2Connections = true` to allow multiple connections when behind a proxy: ```csharp builder.Services .AddGrpcClient(options => { options.Address = new Uri("https://order-service-lb:5001"); }) .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler { EnableMultipleHttp2Connections = true }); ``` --- ## Health Checks ### gRPC Health Check Protocol Implement the standard gRPC health checking protocol (`grpc.health.v1.Health`) so orchestrators and load balancers can probe service status: ```csharp builder.Services.AddGrpc(); builder.Services.AddGrpcHealthChecks() .AddCheck("database", () => { // Custom health check logic return HealthCheckResult.Healthy(); }); var app = builder.Build(); app.MapGrpcService(); app.MapGrpcHealthChecksService(); ``` ### Integration with ASP.NET Core Health Checks gRPC health checks integrate with the standard ASP.NET Core health check system: ```csharp builder.Services.AddHealthChecks() .AddNpgSql( builder.Configuration.GetConnectionString("OrderDb")!, name: "order-db", tags: ["ready"]); builder.Services.AddGrpc(); builder.Services.AddGrpcHealthChecks() .AddAsyncCheck("order-db", async (sp, ct) => { var healthCheckService = sp.GetRequiredService(); var report = await healthCheckService.CheckHealthAsync( r => r.Tags.Contains("ready"), ct); return report.Status == HealthStatus.Healthy ? HealthCheckResult.Healthy() : HealthCheckResult.Unhealthy(); }); ``` ### Kubernetes Probes for gRPC ```yaml # Use grpc health check probe (Kubernetes 1.24+) livenessProbe: grpc: port: 5001 initialDelaySeconds: 10 periodSeconds: 15 readinessProbe: grpc: port: 5001 initialDelaySeconds: 5 periodSeconds: 10 ``` --- ## Interceptors gRPC interceptors are middleware for gRPC calls, analogous to ASP.NET Core middleware or HTTP DelegatingHandlers. ### Server Interceptor ```csharp public sealed class LoggingInterceptor(ILogger logger) : Interceptor { public override async Task UnaryServerHandler( TRequest request, ServerCallContext context, UnaryServerMethod continuation) { var stopwatch = Stopwatch.StartNew(); try { var response = await continuation(request, context); logger.LogInformation( "gRPC {Method} completed in {ElapsedMs}ms", context.Method, stopwatch.ElapsedMilliseconds); return response; } catch (RpcException ex) { logger.LogError(ex, "gRPC {Method} failed with {StatusCode}", context.Method, ex.StatusCode); throw; } } } // Register builder.Services.AddGrpc(options => { options.Interceptors.Add(); }); ``` ### Client Interceptor ```csharp public sealed class AuthInterceptor(ITokenProvider tokenProvider) : Interceptor { public override AsyncUnaryCall AsyncUnaryCall( TRequest request, ClientInterceptorContext context, AsyncUnaryCallContinuation continuation) { var token = tokenProvider.GetCachedToken(); var headers = context.Options.Headers ?? new Metadata(); headers.Add("Authorization", $"Bearer {token}"); var newContext = new ClientInterceptorContext( context.Method, context.Host, context.Options.WithHeaders(headers)); return continuation(request, newContext); } } ``` --- ## Error Handling ### Status Codes Map domain errors to gRPC status codes: | gRPC Status | HTTP Equivalent | Use When | |-------------|----------------|----------| | `OK` | 200 | Success | | `NotFound` | 404 | Resource does not exist | | `InvalidArgument` | 400 | Client sent bad data | | `PermissionDenied` | 403 | Caller lacks permission | | `Unauthenticated` | 401 | No valid credentials | | `AlreadyExists` | 409 | Duplicate creation attempt | | `ResourceExhausted` | 429 | Rate limited | | `Internal` | 500 | Unhandled server error | | `Unavailable` | 503 | Transient failure -- safe to retry | | `DeadlineExceeded` | 504 | Operation timed out | ### Rich Error Details ```csharp // Server: throw with metadata var status = new Status(StatusCode.InvalidArgument, "Validation failed"); var metadata = new Metadata { { "field", "customer_id" }, { "reason", "Customer ID is required" } }; throw new RpcException(status, metadata); // Client: read error metadata try { var response = await client.GetOrderAsync(request); } catch (RpcException ex) when (ex.StatusCode == StatusCode.InvalidArgument) { var field = ex.Trailers.GetValue("field"); var reason = ex.Trailers.GetValue("reason"); logger.LogWarning("Validation error on {Field}: {Reason}", field, reason); } ``` --- ## Deadlines and Cancellation Always set deadlines on gRPC calls to prevent indefinite waits: ```csharp // Client: set a deadline var deadline = DateTime.UtcNow.AddSeconds(10); var response = await client.GetOrderAsync( new GetOrderRequest { Id = 42 }, deadline: deadline); // Server: check deadline and propagate cancellation public override async Task GetOrder( GetOrderRequest request, ServerCallContext context) { // context.CancellationToken is automatically cancelled when deadline expires var order = await repository.GetByIdAsync(request.Id, context.CancellationToken); // ... } ``` --- ## gRPC-Web for Browser Clients Browsers do not support HTTP/2 trailers required by native gRPC. gRPC-Web is a protocol variant that works over HTTP/1.1 and HTTP/2 without trailers, enabling browser JavaScript clients to call gRPC services. ### Server Configuration ```csharp var builder = WebApplication.CreateBuilder(args); builder.Services.AddGrpc(); builder.Services.AddCors(options => { options.AddPolicy("GrpcWeb", policy => { policy.WithOrigins("https://app.example.com") .AllowAnyHeader() .AllowAnyMethod() .WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding"); }); }); var app = builder.Build(); app.UseRouting(); app.UseCors(); app.UseGrpcWeb(); // Must be between UseRouting and MapGrpcService app.MapGrpcService() .EnableGrpcWeb() .RequireCors("GrpcWeb"); ``` ### JavaScript Client (grpc-web) ```javascript // Using @improbable-eng/grpc-web or grpc-web package import { OrderServiceClient } from './generated/order_grpc_web_pb'; import { GetOrderRequest } from './generated/order_pb'; const client = new OrderServiceClient('https://api.example.com'); const request = new GetOrderRequest(); request.setId(42); client.getOrder(request, {}, (err, response) => { if (err) { console.error('gRPC error:', err.message); return; } console.log('Order:', response.toObject()); }); ``` ### Envoy Proxy Alternative Instead of ASP.NET Core gRPC-Web middleware, you can use an Envoy proxy to translate gRPC-Web requests to native gRPC. This is useful when the gRPC service cannot be modified: ```yaml # Envoy filter configuration http_filters: - name: envoy.filters.http.grpc_web typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb - name: envoy.filters.http.cors typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router ``` ### gRPC-Web Limitations - **Unary and server streaming only** -- client streaming and bidirectional streaming are not supported by gRPC-Web - **No HTTP/2 trailers** -- status and trailing metadata are encoded in the response body - **CORS required** -- cross-origin requests need explicit CORS configuration on the server - **Consider SignalR for full-duplex browser communication** -- see [skill:dotnet-realtime-communication] for alternatives when bidirectional streaming is required --- ## Key Principles - **Use `.proto` files as the contract** -- they are the single source of truth for the API shape, shared between client and server - **Set `GrpcServices` on `` items** -- `Server` for service projects, `Client` for consumer projects, `Both` for shared contracts - **Reuse channels** -- `GrpcChannel` manages HTTP/2 connections; creating a new channel per call wastes resources - **Register gRPC clients via DI** -- `AddGrpcClient` integrates with `IHttpClientFactory` for connection pooling and resilience - **Always set deadlines** -- calls without deadlines can hang indefinitely if the server is slow or unreachable - **Use L7 load balancers** -- L4 load balancers pin all traffic to one backend because HTTP/2 multiplexes on a single TCP connection - **Implement the gRPC health check protocol** -- enables Kubernetes probes and load balancers to monitor service health - **Use gRPC-Web for browser clients** -- native gRPC requires HTTP/2 trailers which browsers do not support; gRPC-Web bridges this gap See [skill:dotnet-native-aot] for Native AOT compilation pipeline and [skill:dotnet-aot-architecture] for AOT-compatible patterns when building gRPC services with ahead-of-time compilation. --- ## Agent Gotchas 1. **Do not create a new `GrpcChannel` per request** -- channels are expensive to create and manage HTTP/2 connections. Reuse them or use DI-registered clients. 2. **Do not omit `GrpcServices` on `` items** -- the default is `Both`, which generates server and client stubs. This bloats client projects with unused server code and vice versa. 3. **Do not use L4 load balancers for gRPC without enabling `EnableMultipleHttp2Connections`** -- HTTP/2 multiplexing means a single connection handles all RPCs, defeating load distribution. 4. **Do not throw generic `Exception` from gRPC services** -- throw `RpcException` with appropriate `StatusCode` and descriptive messages. Unhandled exceptions become `StatusCode.Internal` with no useful detail. 5. **Do not forget to call `CompleteAsync()` on client streams** -- the server waits for stream completion before sending its response. Forgetting this causes the call to hang. 6. **Do not use `grpc.health.v1.Health` without registering health checks** -- an empty health service always reports `Serving`, which defeats the purpose of health monitoring. 7. **Do not enable gRPC-Web globally without CORS** -- `UseGrpcWeb()` without a CORS policy allows any origin to call your gRPC services. Always pair with explicit `RequireCors()`. 8. **Do not attempt client streaming or bidirectional streaming with gRPC-Web** -- the gRPC-Web protocol only supports unary and server streaming. Use SignalR or native gRPC for full-duplex browser communication. --- ## Attribution Adapted from [Aaronontheweb/dotnet-skills](https://github.com/Aaronontheweb/dotnet-skills) (MIT license). --- ## References - [gRPC for .NET overview](https://learn.microsoft.com/en-us/aspnet/core/grpc/?view=aspnetcore-10.0) - [Create a gRPC client and server](https://learn.microsoft.com/en-us/aspnet/core/tutorials/grpc/grpc-start?view=aspnetcore-10.0) - [gRPC client factory integration](https://learn.microsoft.com/en-us/aspnet/core/grpc/clientfactory?view=aspnetcore-10.0) - [gRPC services with ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/grpc/aspnetcore?view=aspnetcore-10.0) - [gRPC health checks](https://learn.microsoft.com/en-us/aspnet/core/grpc/health-checks?view=aspnetcore-10.0) - [gRPC load balancing](https://learn.microsoft.com/en-us/aspnet/core/grpc/loadbalancing?view=aspnetcore-10.0) - [gRPC authentication](https://learn.microsoft.com/en-us/aspnet/core/grpc/authn-and-authz?view=aspnetcore-10.0) - [gRPC interceptors](https://learn.microsoft.com/en-us/aspnet/core/grpc/interceptors?view=aspnetcore-10.0) - [gRPC-Web for .NET](https://learn.microsoft.com/en-us/aspnet/core/grpc/grpcweb?view=aspnetcore-10.0) - [Protocol Buffers language guide](https://protobuf.dev/programming-guides/proto3/)