Claude Skill

dotnet-grpc

Build or review gRPC services and clients in .NET with correct contract-first design, streaming behavior, transport assumptions, and backend service integration.

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download postpartum-genushyacinthus29-dotnet-skills-skills_dotnet-grpc-bfa4ebd.zip · 17 KB
Part of postpartum-genushyacinthus29/dotnet-skills — 80 skills

Install

skills CLI npx skills add https://github.com/Postpartum-genushyacinthus29/dotnet-skills/tree/main/skills/dotnet-grpc
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install postpartum-genushyacinthus29-dotnet-skills@llmmart
Git git clone https://github.com/Postpartum-genushyacinthus29/dotnet-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole postpartum-genushyacinthus29/dotnet-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

gRPC for .NET

Trigger On

  • building backend-to-backend RPC services or clients
  • adding protobuf contracts, streaming calls, or interceptors
  • deciding between gRPC, HTTP APIs, and SignalR
  • optimizing gRPC performance and connection management
  • implementing service-to-service communication in microservices

Documentation

References

  • patterns.md - Detailed proto patterns, streaming implementations, interceptors, health checks, and load balancing
  • anti-patterns.md - Common gRPC mistakes with explanations and corrections

Workflow

  1. Use gRPC where low-latency backend communication, strong contracts, or streaming are the real drivers.
  2. Treat .proto files as source of truth and keep generated code ownership clear.
  3. Choose unary, server streaming, client streaming, or bidirectional streaming based on the interaction model, not by default.
  4. Do not use gRPC for broad browser-facing APIs unless the limitations and gRPC-Web tradeoffs are explicitly acceptable.
  5. Handle deadlines, cancellation, auth, and retry behavior explicitly on both server and client paths.
  6. Validate contract changes carefully because gRPC drift breaks callers fast.

Service Patterns

Basic Unary Service

// greeter.proto
syntax = "proto3";

option csharp_namespace = "GrpcService";

package greet;

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

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}
// GreeterService.cs
public class GreeterService : Greeter.GreeterBase
{
    private readonly ILogger<GreeterService> _logger;

    public GreeterService(ILogger<GreeterService> logger)
    {
        _logger = logger;
    }

    public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        _logger.LogInformation("Greeting {Name}", request.Name);
        return Task.FromResult(new HelloReply
        {
            Message = $"Hello {request.Name}"
        });
    }
}

Server Streaming

// In .proto file
service DataStream {
  rpc StreamData (DataRequest) returns (stream DataChunk);
}

// Service implementation
public override async Task StreamData(
    DataRequest request,
    IServerStreamWriter<DataChunk> responseStream,
    ServerCallContext context)
{
    for (int i = 0; i < request.Count; i++)
    {
        // Check for cancellation to avoid wasted work
        if (context.CancellationToken.IsCancellationRequested)
        {
            _logger.LogInformation("Stream cancelled by client");
            break;
        }

        await responseStream.WriteAsync(new DataChunk
        {
            Index = i,
            Data = await GetDataAsync(i)
        });

        // Respect backpressure
        await Task.Delay(10, context.CancellationToken);
    }
}

Bidirectional Streaming

// In .proto file
service Chat {
  rpc ChatStream (stream ChatMessage) returns (stream ChatMessage);
}

// Service implementation
public override async Task ChatStream(
    IAsyncStreamReader<ChatMessage> requestStream,
    IServerStreamWriter<ChatMessage> responseStream,
    ServerCallContext context)
{
    await foreach (var message in requestStream.ReadAllAsync(context.CancellationToken))
    {
        _logger.LogInformation("Received: {Message}", message.Text);

        // Echo back with transformation
        await responseStream.WriteAsync(new ChatMessage
        {
            Text = $"Echo: {message.Text}",
            Timestamp = Timestamp.FromDateTime(DateTime.UtcNow)
        });
    }
}

Client Patterns

Channel Reuse with Client Factory (Recommended)

// Program.cs - Register gRPC client with factory
builder.Services.AddGrpcClient<Greeter.GreeterClient>(options =>
{
    options.Address = new Uri("https://localhost:5001");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
    var handler = new SocketsHttpHandler
    {
        PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
        KeepAlivePingDelay = TimeSpan.FromSeconds(60),
        KeepAlivePingTimeout = TimeSpan.FromSeconds(30),
        EnableMultipleHttp2Connections = true
    };
    return handler;
})
.AddInterceptor<LoggingInterceptor>();

// Usage in service
public class MyService
{
    private readonly Greeter.GreeterClient _client;

    public MyService(Greeter.GreeterClient client)
    {
        _client = client;
    }

    public async Task<string> GreetAsync(string name, CancellationToken ct)
    {
        // Always set deadlines
        var deadline = DateTime.UtcNow.AddSeconds(5);
        var response = await _client.SayHelloAsync(
            new HelloRequest { Name = name },
            deadline: deadline,
            cancellationToken: ct);
        return response.Message;
    }
}

Manual Channel Creation with Connection Options

// Reuse channels - expensive to create
var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
{
    HttpHandler = new SocketsHttpHandler
    {
        EnableMultipleHttp2Connections = true,
        PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
        KeepAlivePingDelay = TimeSpan.FromSeconds(60),
        KeepAlivePingTimeout = TimeSpan.FromSeconds(30)
    },
    MaxRetryAttempts = 3,
    ServiceConfig = new ServiceConfig
    {
        MethodConfigs =
        {
            new MethodConfig
            {
                Names = { MethodName.Default },
                RetryPolicy = new RetryPolicy
                {
                    MaxAttempts = 3,
                    InitialBackoff = TimeSpan.FromSeconds(1),
                    MaxBackoff = TimeSpan.FromSeconds(5),
                    BackoffMultiplier = 1.5,
                    RetryableStatusCodes = { StatusCode.Unavailable }
                }
            }
        }
    }
});

// Create multiple clients from same channel
var greeterClient = new Greeter.GreeterClient(channel);
var orderClient = new Orders.OrdersClient(channel);

Consuming Server Streaming

public async Task ProcessStreamAsync(CancellationToken ct)
{
    using var call = _client.StreamData(new DataRequest { Count = 100 });

    try
    {
        await foreach (var chunk in call.ResponseStream.ReadAllAsync(ct))
        {
            await ProcessChunkAsync(chunk);
        }
    }
    catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled)
    {
        _logger.LogInformation("Stream cancelled");
    }
}

Bidirectional Streaming Client

public async Task ChatAsync(CancellationToken ct)
{
    using var call = _client.ChatStream();

    // Read responses in background
    var readTask = Task.Run(async () =>
    {
        await foreach (var response in call.ResponseStream.ReadAllAsync(ct))
        {
            Console.WriteLine($"Received: {response.Text}");
        }
    }, ct);

    // Send messages
    foreach (var message in GetMessages())
    {
        if (ct.IsCancellationRequested) break;

        await call.RequestStream.WriteAsync(new ChatMessage { Text = message });
    }

    // Signal completion and wait for responses
    await call.RequestStream.CompleteAsync();
    await readTask;
}

Interceptor Patterns

Logging Interceptor

public class LoggingInterceptor : Interceptor
{
    private readonly ILogger<LoggingInterceptor> _logger;

    public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
    {
        _logger = logger;
    }

    public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
        TRequest request,
        ClientInterceptorContext<TRequest, TResponse> context,
        AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
    {
        var sw = Stopwatch.StartNew();
        var call = continuation(request, context);

        return new AsyncUnaryCall<TResponse>(
            HandleResponse(call.ResponseAsync, context.Method.FullName, sw),
            call.ResponseHeadersAsync,
            call.GetStatus,
            call.GetTrailers,
            call.Dispose);
    }

    private async Task<TResponse> HandleResponse<TResponse>(
        Task<TResponse> responseTask, string method, Stopwatch sw)
    {
        try
        {
            var response = await responseTask;
            _logger.LogInformation("{Method} completed in {Elapsed}ms",
                method, sw.ElapsedMilliseconds);
            return response;
        }
        catch (RpcException ex)
        {
            _logger.LogError(ex, "{Method} failed with {Status} in {Elapsed}ms",
                method, ex.StatusCode, sw.ElapsedMilliseconds);
            throw;
        }
    }
}

Server Exception Interceptor

public class ExceptionInterceptor : Interceptor
{
    private readonly ILogger<ExceptionInterceptor> _logger;

    public ExceptionInterceptor(ILogger<ExceptionInterceptor> logger)
    {
        _logger = logger;
    }

    public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
        TRequest request,
        ServerCallContext context,
        UnaryServerMethod<TRequest, TResponse> continuation)
    {
        try
        {
            return await continuation(request, context);
        }
        catch (RpcException)
        {
            throw; // Let gRPC exceptions pass through
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Unhandled exception in {Method}", context.Method);
            throw new RpcException(new Status(StatusCode.Internal, "An error occurred"));
        }
    }
}

Server Configuration

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc(options =>
{
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.MaxReceiveMessageSize = 4 * 1024 * 1024; // 4 MB
    options.MaxSendMessageSize = 4 * 1024 * 1024;
    options.Interceptors.Add<ExceptionInterceptor>();
});

// Configure Kestrel for HTTP/2
builder.WebHost.ConfigureKestrel(options =>
{
    options.Limits.Http2.MaxStreamsPerConnection = 100;
    options.Limits.Http2.InitialConnectionWindowSize = 1024 * 1024; // 1 MB
    options.Limits.Http2.InitialStreamWindowSize = 512 * 1024; // 512 KB
});

var app = builder.Build();

app.MapGrpcService<GreeterService>();
app.MapGet("/", () => "gRPC endpoint");

app.Run();

Anti-Patterns to Avoid

Anti-Pattern Why It's Bad Better Approach
Creating new channel per call Connection overhead kills performance Reuse channels, use client factory
Missing deadlines Calls can hang indefinitely Always set deadline on client calls
Ignoring cancellation in streams Wastes server resources Check CancellationToken periodically
Using gRPC for browser clients Limited browser support Use gRPC-Web with Envoy or REST
Large messages (>1MB) Memory pressure, LOH allocations Stream in chunks or use HTTP for files
Sync blocking (Task.Result) Thread pool starvation Use async/await consistently
Swallowing exceptions in interceptors Hides failures from clients Rethrow or convert to RpcException
Not aligning client/server deadlines Mismatched timeout behavior Coordinate deadline budgets
Blocking AsyncUnaryCall with BlockingUnaryCall interceptor Interceptors are method-specific Implement both interceptor methods
Missing retry configuration Single failures cause request failure Configure retry policy on channel

Best Practices

Channel and Connection Management

  1. Reuse channels across the application lifetime
  2. Enable multiple HTTP/2 connections with EnableMultipleHttp2Connections = true
  3. Configure keep-alive pings to maintain connections through idle periods
  4. Use client factory (AddGrpcClient) for centralized channel management
  5. Set PooledConnectionIdleTimeout to prevent premature connection closure

Deadlines and Cancellation

  1. Always set deadlines on client calls to prevent indefinite hangs
  2. Propagate cancellation through the call chain
  3. Check cancellation in long-running streaming handlers
  4. Coordinate deadline budgets between client and server

Performance

  1. Avoid large messages (>85KB to stay off Large Object Heap)
  2. Use streaming for large data transfers instead of single messages
  3. Enable server GC for high-throughput client applications
  4. Complete streams gracefully to allow connection reuse
  5. Dispose streaming calls when done to release resources

Error Handling

  1. Use appropriate status codes (not just Internal for everything)
  2. Let RpcException propagate through interceptors
  3. Convert domain exceptions to gRPC status codes at service boundaries
  4. Include meaningful error details in development mode only

Contract Design

  1. Use custom objects in proto to enable backward-compatible evolution
  2. Reserve field numbers you remove instead of reusing
  3. Version service names for breaking changes (GreeterV2)
  4. Keep proto files as the single source of truth

Observability

  1. Add logging interceptors for request/response timing
  2. Track error rates by status code
  3. Monitor connection pool health and reuse rates
  4. Integrate with distributed tracing (OpenTelemetry)

Deliver

  • stable protobuf contracts and generated code flow
  • service and client code that match the RPC shape
  • tests or smoke checks for serialization and call behavior
  • proper deadline and cancellation handling

Validate

  • gRPC is chosen for the right problem
  • streaming semantics and deadlines are explicit
  • browser constraints are acknowledged when relevant
  • channels are reused appropriately
  • error handling converts exceptions to proper status codes
  • interceptors are ordered correctly (logging before auth before validation)
Files (dotnet-skills)
  • references
    • anti-patterns.md 24.6 KB
      # gRPC Anti-Patterns Reference
      
      This document catalogs common mistakes when building gRPC services and clients in .NET, with explanations and corrections.
      
      ## Connection and Channel Anti-Patterns
      
      ### Creating a New Channel Per Call
      
      ```csharp
      // WRONG: Creating channel per call
      public async Task<string> GetGreetingAsync(string name)
      {
          using var channel = GrpcChannel.ForAddress("https://localhost:5001");
          var client = new Greeter.GreeterClient(channel);
          var reply = await client.SayHelloAsync(new HelloRequest { Name = name });
          return reply.Message;
      }
      ```
      
      **Why it's bad:**
      - HTTP/2 connection establishment is expensive (TLS handshake, SETTINGS exchange)
      - Prevents connection pooling and multiplexing
      - Kills performance under load
      - Causes connection churn on the server
      
      **Correct approach:**
      ```csharp
      // CORRECT: Use client factory for channel reuse
      builder.Services.AddGrpcClient<Greeter.GreeterClient>(options =>
      {
          options.Address = new Uri("https://localhost:5001");
      });
      
      // Or manage singleton channel manually
      public class GrpcClientService
      {
          private static readonly GrpcChannel _channel =
              GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
              {
                  HttpHandler = new SocketsHttpHandler
                  {
                      EnableMultipleHttp2Connections = true,
                      PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
                      KeepAlivePingDelay = TimeSpan.FromSeconds(60),
                      KeepAlivePingTimeout = TimeSpan.FromSeconds(30)
                  }
              });
      
          private readonly Greeter.GreeterClient _client = new(_channel);
      
          public Task<HelloReply> SayHelloAsync(HelloRequest request) =>
              _client.SayHelloAsync(request).ResponseAsync;
      }
      ```
      
      ### Disposing Channel After Every Call
      
      ```csharp
      // WRONG: Disposing channel immediately
      public async Task DoWorkAsync()
      {
          var channel = GrpcChannel.ForAddress("https://localhost:5001");
          try
          {
              var client = new Greeter.GreeterClient(channel);
              await client.SayHelloAsync(new HelloRequest { Name = "World" });
          }
          finally
          {
              await channel.ShutdownAsync(); // Kills connection reuse
          }
      }
      ```
      
      **Why it's bad:**
      - ShutdownAsync closes all HTTP/2 connections
      - Subsequent calls must re-establish connections
      - Negates HTTP/2 multiplexing benefits
      
      **Correct approach:**
      ```csharp
      // CORRECT: Channel lives for application lifetime
      public class ChannelManager : IAsyncDisposable
      {
          private readonly GrpcChannel _channel;
      
          public ChannelManager(string address)
          {
              _channel = GrpcChannel.ForAddress(address);
          }
      
          public GrpcChannel Channel => _channel;
      
          public async ValueTask DisposeAsync()
          {
              await _channel.ShutdownAsync();
          }
      }
      
      // Register as singleton
      builder.Services.AddSingleton(sp =>
          new ChannelManager("https://localhost:5001"));
      ```
      
      ## Deadline and Timeout Anti-Patterns
      
      ### Missing Deadlines on Client Calls
      
      ```csharp
      // WRONG: No deadline set
      var response = await client.ProcessOrderAsync(new OrderRequest { OrderId = orderId });
      ```
      
      **Why it's bad:**
      - Calls can hang indefinitely if server is slow or network fails
      - No way to enforce timeout at the gRPC level
      - Can cause thread/connection exhaustion under load
      
      **Correct approach:**
      ```csharp
      // CORRECT: Always set a deadline
      var deadline = DateTime.UtcNow.AddSeconds(10);
      var response = await client.ProcessOrderAsync(
          new OrderRequest { OrderId = orderId },
          deadline: deadline);
      
      // Or use CallOptions for more control
      var options = new CallOptions(deadline: DateTime.UtcNow.AddSeconds(10));
      var response = await client.ProcessOrderAsync(new OrderRequest { OrderId = orderId }, options);
      ```
      
      ### Using CancellationToken Instead of Deadline
      
      ```csharp
      // WRONG: CancellationToken alone doesn't signal the server
      var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
      var response = await client.ProcessOrderAsync(request, cancellationToken: cts.Token);
      ```
      
      **Why it's bad:**
      - CancellationToken only cancels client-side
      - Server continues processing, wasting resources
      - Deadline is transmitted to server, CancellationToken is not
      
      **Correct approach:**
      ```csharp
      // CORRECT: Use deadline AND cancellation token
      var deadline = DateTime.UtcNow.AddSeconds(10);
      var response = await client.ProcessOrderAsync(
          request,
          deadline: deadline,
          cancellationToken: cancellationToken);
      ```
      
      ### Mismatched Client/Server Deadlines
      
      ```csharp
      // WRONG: Server deadline longer than client
      // Client sets 5 second deadline
      var deadline = DateTime.UtcNow.AddSeconds(5);
      var response = await client.ProcessAsync(request, deadline: deadline);
      
      // Server handler takes up to 30 seconds
      public override async Task<Response> Process(Request request, ServerCallContext context)
      {
          await LongRunningOperation(TimeSpan.FromSeconds(30)); // Client already timed out
          return new Response();
      }
      ```
      
      **Why it's bad:**
      - Server wastes resources after client has given up
      - Results are computed but never delivered
      - No coordinated timeout behavior
      
      **Correct approach:**
      ```csharp
      // CORRECT: Server respects incoming deadline
      public override async Task<Response> Process(Request request, ServerCallContext context)
      {
          // Check remaining time before expensive operations
          var remaining = context.Deadline - DateTime.UtcNow;
          if (remaining < TimeSpan.FromSeconds(1))
          {
              throw new RpcException(new Status(StatusCode.DeadlineExceeded, "Insufficient time"));
          }
      
          using var cts = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken);
          cts.CancelAfter(remaining - TimeSpan.FromMilliseconds(100)); // Buffer for response
      
          await ProcessWithTimeoutAsync(request, cts.Token);
          return new Response();
      }
      ```
      
      ## Streaming Anti-Patterns
      
      ### Ignoring Cancellation in Streaming Handlers
      
      ```csharp
      // WRONG: Not checking cancellation
      public override async Task StreamData(
          DataRequest request,
          IServerStreamWriter<DataChunk> responseStream,
          ServerCallContext context)
      {
          var items = await GetAllItemsAsync(); // Could be millions
          foreach (var item in items)
          {
              await responseStream.WriteAsync(item); // Continues even if client disconnects
          }
      }
      ```
      
      **Why it's bad:**
      - Server continues processing after client disconnects
      - Wastes CPU, memory, and network resources
      - Can cause resource exhaustion under load
      
      **Correct approach:**
      ```csharp
      // CORRECT: Check cancellation regularly
      public override async Task StreamData(
          DataRequest request,
          IServerStreamWriter<DataChunk> responseStream,
          ServerCallContext context)
      {
          await foreach (var item in GetItemsAsync(context.CancellationToken))
          {
              context.CancellationToken.ThrowIfCancellationRequested();
              await responseStream.WriteAsync(item, context.CancellationToken);
          }
      }
      ```
      
      ### Not Completing Client Streams
      
      ```csharp
      // WRONG: Forgetting to complete the request stream
      public async Task UploadDataAsync(IEnumerable<DataChunk> chunks)
      {
          using var call = _client.UploadData();
      
          foreach (var chunk in chunks)
          {
              await call.RequestStream.WriteAsync(chunk);
          }
      
          // Missing: await call.RequestStream.CompleteAsync();
          var response = await call.ResponseAsync; // Hangs forever
      }
      ```
      
      **Why it's bad:**
      - Server waits indefinitely for more messages
      - Call never completes
      - Resources are held until timeout
      
      **Correct approach:**
      ```csharp
      // CORRECT: Always complete client streams
      public async Task UploadDataAsync(IEnumerable<DataChunk> chunks)
      {
          using var call = _client.UploadData();
      
          foreach (var chunk in chunks)
          {
              await call.RequestStream.WriteAsync(chunk);
          }
      
          await call.RequestStream.CompleteAsync(); // Signal end of stream
          var response = await call.ResponseAsync;
      }
      ```
      
      ### Blocking on Bidirectional Streams
      
      ```csharp
      // WRONG: Sequential read/write in bidirectional stream
      public async Task ChatAsync()
      {
          using var call = _client.Chat();
      
          while (true)
          {
              var message = await GetNextMessageAsync();
              await call.RequestStream.WriteAsync(message);
      
              // Blocks until response arrives - can't send another message until then
              var response = await call.ResponseStream.MoveNext();
          }
      }
      ```
      
      **Why it's bad:**
      - Serializes what should be concurrent operations
      - Loses bidirectional streaming benefits
      - Can deadlock if server expects multiple requests before responding
      
      **Correct approach:**
      ```csharp
      // CORRECT: Concurrent read and write
      public async Task ChatAsync(CancellationToken ct)
      {
          using var call = _client.Chat();
      
          var readTask = Task.Run(async () =>
          {
              await foreach (var response in call.ResponseStream.ReadAllAsync(ct))
              {
                  ProcessResponse(response);
              }
          }, ct);
      
          var writeTask = Task.Run(async () =>
          {
              await foreach (var message in GetMessagesAsync(ct))
              {
                  await call.RequestStream.WriteAsync(message);
              }
              await call.RequestStream.CompleteAsync();
          }, ct);
      
          await Task.WhenAll(readTask, writeTask);
      }
      ```
      
      ## Message Size Anti-Patterns
      
      ### Sending Large Messages
      
      ```csharp
      // WRONG: Sending multi-MB payloads
      message FileUploadRequest {
        bytes content = 1; // Could be hundreds of MB
        string filename = 2;
      }
      
      public async Task UploadFileAsync(byte[] fileContent)
      {
          await _client.UploadFileAsync(new FileUploadRequest
          {
              Content = ByteString.CopyFrom(fileContent), // LOH allocation
              Filename = "large-file.zip"
          });
      }
      ```
      
      **Why it's bad:**
      - Large messages cause Large Object Heap allocations
      - Memory fragmentation and GC pressure
      - Can exceed default message size limits (4MB)
      - Single-request timeout affects entire transfer
      
      **Correct approach:**
      ```csharp
      // CORRECT: Use streaming for large data
      service FileService {
        rpc UploadFile (stream FileChunk) returns (UploadResponse);
      }
      
      message FileChunk {
        oneof data {
          FileMetadata metadata = 1;
          bytes chunk = 2;
        }
      }
      
      public async Task UploadFileAsync(string filePath, CancellationToken ct)
      {
          using var call = _client.UploadFile();
      
          // Send metadata first
          await call.RequestStream.WriteAsync(new FileChunk
          {
              Metadata = new FileMetadata { Filename = Path.GetFileName(filePath) }
          });
      
          // Stream file in chunks
          await using var stream = File.OpenRead(filePath);
          var buffer = new byte[64 * 1024]; // 64KB chunks
          int bytesRead;
      
          while ((bytesRead = await stream.ReadAsync(buffer, ct)) > 0)
          {
              await call.RequestStream.WriteAsync(new FileChunk
              {
                  Chunk = ByteString.CopyFrom(buffer, 0, bytesRead)
              });
          }
      
          await call.RequestStream.CompleteAsync();
          var response = await call.ResponseAsync;
      }
      ```
      
      ## Error Handling Anti-Patterns
      
      ### Swallowing Exceptions
      
      ```csharp
      // WRONG: Swallowing exceptions returns OK status
      public override async Task<OrderResponse> CreateOrder(OrderRequest request, ServerCallContext context)
      {
          try
          {
              await _repository.CreateOrderAsync(request);
              return new OrderResponse { Success = true };
          }
          catch (Exception ex)
          {
              _logger.LogError(ex, "Order creation failed");
              return new OrderResponse { Success = false }; // Client sees OK status
          }
      }
      ```
      
      **Why it's bad:**
      - Client receives OK status despite failure
      - Error details lost in response message
      - Client retry logic doesn't trigger
      - Breaks gRPC error handling conventions
      
      **Correct approach:**
      ```csharp
      // CORRECT: Convert exceptions to appropriate status codes
      public override async Task<OrderResponse> CreateOrder(OrderRequest request, ServerCallContext context)
      {
          try
          {
              await _repository.CreateOrderAsync(request);
              return new OrderResponse { Success = true };
          }
          catch (ValidationException ex)
          {
              throw new RpcException(new Status(StatusCode.InvalidArgument, ex.Message));
          }
          catch (DuplicateOrderException ex)
          {
              throw new RpcException(new Status(StatusCode.AlreadyExists, ex.Message));
          }
          catch (Exception ex)
          {
              _logger.LogError(ex, "Order creation failed");
              throw new RpcException(new Status(StatusCode.Internal, "An error occurred"));
          }
      }
      ```
      
      ### Using Wrong Status Codes
      
      ```csharp
      // WRONG: Using Internal for everything
      catch (KeyNotFoundException)
      {
          throw new RpcException(new Status(StatusCode.Internal, "Not found")); // Should be NotFound
      }
      catch (UnauthorizedAccessException)
      {
          throw new RpcException(new Status(StatusCode.Internal, "Access denied")); // Should be PermissionDenied
      }
      catch (ArgumentException)
      {
          throw new RpcException(new Status(StatusCode.Internal, "Bad argument")); // Should be InvalidArgument
      }
      ```
      
      **Why it's bad:**
      - Clients can't distinguish error types
      - Retry policies may retry non-retryable errors
      - Metrics and monitoring lose granularity
      
      **Correct approach:**
      ```csharp
      // CORRECT: Map to appropriate gRPC status codes
      public static class ExceptionMapping
      {
          public static RpcException ToRpcException(Exception ex) => ex switch
          {
              ArgumentException e => new RpcException(new Status(StatusCode.InvalidArgument, e.Message)),
              KeyNotFoundException e => new RpcException(new Status(StatusCode.NotFound, e.Message)),
              UnauthorizedAccessException e => new RpcException(new Status(StatusCode.PermissionDenied, e.Message)),
              InvalidOperationException e => new RpcException(new Status(StatusCode.FailedPrecondition, e.Message)),
              NotImplementedException e => new RpcException(new Status(StatusCode.Unimplemented, e.Message)),
              OperationCanceledException e => new RpcException(new Status(StatusCode.Cancelled, e.Message)),
              TimeoutException e => new RpcException(new Status(StatusCode.DeadlineExceeded, e.Message)),
              _ => new RpcException(new Status(StatusCode.Internal, "An internal error occurred"))
          };
      }
      ```
      
      ### Leaking Sensitive Information in Error Details
      
      ```csharp
      // WRONG: Including stack trace and internal details
      catch (Exception ex)
      {
          throw new RpcException(new Status(
              StatusCode.Internal,
              $"Failed: {ex.Message}\n{ex.StackTrace}\nConnection: {_connectionString}"));
      }
      ```
      
      **Why it's bad:**
      - Exposes internal implementation details
      - May leak sensitive data (connection strings, paths)
      - Security vulnerability
      
      **Correct approach:**
      ```csharp
      // CORRECT: Log details server-side, return safe message
      catch (Exception ex)
      {
          var errorId = Guid.NewGuid();
          _logger.LogError(ex, "Error {ErrorId}: {Message}", errorId, ex.Message);
      
          var message = _environment.IsDevelopment()
              ? ex.Message
              : $"An error occurred. Reference: {errorId}";
      
          throw new RpcException(new Status(StatusCode.Internal, message));
      }
      ```
      
      ## Interceptor Anti-Patterns
      
      ### Blocking in Interceptors
      
      ```csharp
      // WRONG: Synchronous blocking in async interceptor
      public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
          TRequest request,
          ClientInterceptorContext<TRequest, TResponse> context,
          AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
      {
          // Blocking call - ties up thread pool
          var token = _tokenService.GetTokenAsync().Result; // NEVER do this
      
          var headers = context.Options.Headers ?? new Metadata();
          headers.Add("authorization", $"Bearer {token}");
      
          return continuation(request, context);
      }
      ```
      
      **Why it's bad:**
      - Blocks thread pool threads
      - Can cause thread pool starvation
      - Degrades application throughput
      
      **Correct approach:**
      ```csharp
      // CORRECT: Handle async properly in interceptor
      public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
          TRequest request,
          ClientInterceptorContext<TRequest, TResponse> context,
          AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
      {
          // Get token synchronously if possible, or wrap the response
          var call = continuation(request, context);
      
          return new AsyncUnaryCall<TResponse>(
              AddAuthAndCallAsync(request, context, call.ResponseAsync),
              call.ResponseHeadersAsync,
              call.GetStatus,
              call.GetTrailers,
              call.Dispose);
      }
      
      private async Task<TResponse> AddAuthAndCallAsync<TRequest, TResponse>(
          TRequest request,
          ClientInterceptorContext<TRequest, TResponse> context,
          Task<TResponse> responseTask)
      {
          // Can await safely here
          var token = await _tokenService.GetTokenAsync();
          // Note: Headers are already sent at this point, so this pattern
          // requires restructuring to modify context before continuation
          return await responseTask;
      }
      ```
      
      ### Incomplete Interceptor Implementation
      
      ```csharp
      // WRONG: Only implementing UnaryServerHandler
      public class AuthInterceptor : Interceptor
      {
          public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
              TRequest request,
              ServerCallContext context,
              UnaryServerMethod<TRequest, TResponse> continuation)
          {
              await ValidateAuthAsync(context);
              return await continuation(request, context);
          }
      
          // Missing: ServerStreamingServerHandler, ClientStreamingServerHandler,
          // DuplexStreamingServerHandler - streaming calls bypass auth!
      }
      ```
      
      **Why it's bad:**
      - Streaming calls bypass the interceptor logic
      - Security holes in streaming endpoints
      - Inconsistent behavior
      
      **Correct approach:**
      ```csharp
      // CORRECT: Implement all relevant interceptor methods
      public class AuthInterceptor : Interceptor
      {
          public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
              TRequest request,
              ServerCallContext context,
              UnaryServerMethod<TRequest, TResponse> continuation)
          {
              await ValidateAuthAsync(context);
              return await continuation(request, context);
          }
      
          public override async Task ServerStreamingServerHandler<TRequest, TResponse>(
              TRequest request,
              IServerStreamWriter<TResponse> responseStream,
              ServerCallContext context,
              ServerStreamingServerMethod<TRequest, TResponse> continuation)
          {
              await ValidateAuthAsync(context);
              await continuation(request, responseStream, context);
          }
      
          public override async Task<TResponse> ClientStreamingServerHandler<TRequest, TResponse>(
              IAsyncStreamReader<TRequest> requestStream,
              ServerCallContext context,
              ClientStreamingServerMethod<TRequest, TResponse> continuation)
          {
              await ValidateAuthAsync(context);
              return await continuation(requestStream, context);
          }
      
          public override async Task DuplexStreamingServerHandler<TRequest, TResponse>(
              IAsyncStreamReader<TRequest> requestStream,
              IServerStreamWriter<TResponse> responseStream,
              ServerCallContext context,
              DuplexStreamingServerMethod<TRequest, TResponse> continuation)
          {
              await ValidateAuthAsync(context);
              await continuation(requestStream, responseStream, context);
          }
      }
      ```
      
      ## Proto Design Anti-Patterns
      
      ### Using Primitives for Optional Fields
      
      ```protobuf
      // WRONG: Primitives have default values, not null
      message SearchRequest {
        int32 page_size = 1;    // 0 means unset OR 0?
        bool include_deleted = 2; // false means unset OR false?
      }
      ```
      
      **Why it's bad:**
      - Can't distinguish "not set" from "set to default value"
      - Forces awkward conventions (e.g., -1 means unset)
      - Proto3 removed "required" and made all fields optional with defaults
      
      **Correct approach:**
      ```protobuf
      // CORRECT: Use wrapper types for optional primitives
      import "google/protobuf/wrappers.proto";
      
      message SearchRequest {
        google.protobuf.Int32Value page_size = 1;    // null means unset
        google.protobuf.BoolValue include_deleted = 2; // null means unset
      }
      
      // Or use explicit presence with optional keyword (proto3 optional)
      message SearchRequest {
        optional int32 page_size = 1;
        optional bool include_deleted = 2;
      }
      ```
      
      ### Reusing Field Numbers
      
      ```protobuf
      // VERSION 1
      message User {
        string id = 1;
        string email = 2;
        string phone = 3; // Later removed
      }
      
      // VERSION 2 - WRONG
      message User {
        string id = 1;
        string email = 2;
        string address = 3; // Reused field 3 - breaks wire compatibility
      }
      ```
      
      **Why it's bad:**
      - Old clients deserialize address bytes as phone string
      - Silent data corruption
      - Breaks backward compatibility
      
      **Correct approach:**
      ```protobuf
      // VERSION 2 - CORRECT
      message User {
        string id = 1;
        string email = 2;
        reserved 3;
        reserved "phone";
        string address = 4; // New field number
      }
      ```
      
      ### Flat Request Messages
      
      ```protobuf
      // WRONG: Flat request makes evolution difficult
      service OrderService {
        rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
      }
      
      message CreateOrderRequest {
        string customer_id = 1;
        string product_id = 2;
        int32 quantity = 3;
        string shipping_street = 4;
        string shipping_city = 5;
        string shipping_country = 6;
        // Adding billing address requires many new fields
      }
      ```
      
      **Why it's bad:**
      - Hard to add related groups of fields
      - No reuse across messages
      - Field explosion over time
      
      **Correct approach:**
      ```protobuf
      // CORRECT: Nested messages for structured data
      message CreateOrderRequest {
        string customer_id = 1;
        OrderDetails order = 2;
        Address shipping_address = 3;
        Address billing_address = 4; // Easy to add
      }
      
      message OrderDetails {
        repeated OrderItem items = 1;
      }
      
      message OrderItem {
        string product_id = 1;
        int32 quantity = 2;
      }
      
      message Address {
        string street = 1;
        string city = 2;
        string country = 3;
        string postal_code = 4;
      }
      ```
      
      ## Configuration Anti-Patterns
      
      ### Not Configuring HTTP/2 Limits
      
      ```csharp
      // WRONG: Using defaults that may not match your workload
      var builder = WebApplication.CreateBuilder(args);
      builder.Services.AddGrpc(); // Default limits
      ```
      
      **Why it's bad:**
      - Default stream limits may be too low for your traffic
      - Window sizes may cause unnecessary backpressure
      - Keep-alive not configured for long-lived connections
      
      **Correct approach:**
      ```csharp
      // CORRECT: Configure HTTP/2 settings appropriately
      builder.WebHost.ConfigureKestrel(options =>
      {
          options.Limits.Http2.MaxStreamsPerConnection = 250;
          options.Limits.Http2.InitialConnectionWindowSize = 1024 * 1024; // 1MB
          options.Limits.Http2.InitialStreamWindowSize = 768 * 1024; // 768KB
          options.Limits.Http2.KeepAlivePingDelay = TimeSpan.FromSeconds(30);
          options.Limits.Http2.KeepAlivePingTimeout = TimeSpan.FromSeconds(10);
      });
      ```
      
      ### Ignoring Keep-Alive Configuration
      
      ```csharp
      // WRONG: No keep-alive configuration
      var channel = GrpcChannel.ForAddress("https://server:5001");
      ```
      
      **Why it's bad:**
      - Connections may be silently closed by proxies/load balancers
      - First call after idle period fails
      - No detection of dead connections
      
      **Correct approach:**
      ```csharp
      // CORRECT: Configure keep-alive
      var channel = GrpcChannel.ForAddress("https://server:5001", new GrpcChannelOptions
      {
          HttpHandler = new SocketsHttpHandler
          {
              KeepAlivePingDelay = TimeSpan.FromSeconds(60),
              KeepAlivePingTimeout = TimeSpan.FromSeconds(30),
              PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
              EnableMultipleHttp2Connections = true
          }
      });
      ```
      
      ## Testing Anti-Patterns
      
      ### Testing with Real Network Calls
      
      ```csharp
      // WRONG: Integration test depends on real server
      [Fact]
      public async Task CreateOrder_WithValidData_ReturnsSuccess()
      {
          var channel = GrpcChannel.ForAddress("https://localhost:5001");
          var client = new Orders.OrdersClient(channel);
      
          var response = await client.CreateOrderAsync(new CreateOrderRequest
          {
              CustomerId = "test-customer"
          });
      
          Assert.True(response.Success);
      }
      ```
      
      **Why it's bad:**
      - Tests depend on external server
      - Flaky due to network issues
      - Slow test execution
      - Can't test edge cases easily
      
      **Correct approach:**
      ```csharp
      // CORRECT: Use TestServer or mocks
      [Fact]
      public async Task CreateOrder_WithValidData_ReturnsSuccess()
      {
          await using var factory = new WebApplicationFactory<Program>();
          using var channel = GrpcChannel.ForAddress("http://localhost", new GrpcChannelOptions
          {
              HttpHandler = factory.Server.CreateHandler()
          });
      
          var client = new Orders.OrdersClient(channel);
      
          var response = await client.CreateOrderAsync(new CreateOrderRequest
          {
              CustomerId = "test-customer"
          });
      
          Assert.True(response.Success);
      }
      
      // Or use Moq with generated client interface
      [Fact]
      public async Task ProcessOrder_CallsService()
      {
          var mockClient = new Mock<IOrdersClient>();
          mockClient
              .Setup(x => x.CreateOrderAsync(It.IsAny<CreateOrderRequest>(), null, null, default))
              .Returns(new AsyncUnaryCall<CreateOrderResponse>(
                  Task.FromResult(new CreateOrderResponse { Success = true }),
                  Task.FromResult(new Metadata()),
                  () => Status.DefaultSuccess,
                  () => new Metadata(),
                  () => { }));
      
          var service = new OrderProcessor(mockClient.Object);
          await service.ProcessAsync();
      
          mockClient.Verify(x => x.CreateOrderAsync(
              It.Is<CreateOrderRequest>(r => r.CustomerId == "expected"),
              null, null, default), Times.Once);
      }
      ```
      
    • patterns.md 22.2 KB
      # gRPC Patterns Reference
      
      This document provides detailed patterns for gRPC services in .NET, covering proto design, streaming implementations, and interceptor patterns.
      
      ## Proto File Patterns
      
      ### Service Definition Best Practices
      
      ```protobuf
      syntax = "proto3";
      
      package mycompany.orders.v1;
      
      option csharp_namespace = "MyCompany.Orders.V1";
      
      import "google/protobuf/timestamp.proto";
      import "google/protobuf/wrappers.proto";
      import "google/protobuf/empty.proto";
      
      // Version services explicitly for breaking changes
      service OrderService {
        // Unary RPC for simple request-response
        rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
      
        // Server streaming for large result sets
        rpc ListOrders (ListOrdersRequest) returns (stream OrderItem);
      
        // Client streaming for batch uploads
        rpc UploadOrders (stream CreateOrderRequest) returns (UploadOrdersResponse);
      
        // Bidirectional streaming for real-time sync
        rpc SyncOrders (stream OrderSyncMessage) returns (stream OrderSyncMessage);
      }
      ```
      
      ### Message Design Patterns
      
      ```protobuf
      // Use wrapper types for optional primitives
      message OrderFilter {
        google.protobuf.StringValue customer_id = 1;
        google.protobuf.Int32Value min_quantity = 2;
        google.protobuf.BoolValue is_active = 3;
      }
      
      // Wrap request fields in objects for extensibility
      message CreateOrderRequest {
        OrderDetails order = 1;
        RequestMetadata metadata = 2;
      }
      
      message OrderDetails {
        string customer_id = 1;
        repeated OrderLineItem items = 2;
        ShippingAddress address = 3;
      }
      
      // Use oneof for mutually exclusive fields
      message PaymentMethod {
        oneof method {
          CreditCard credit_card = 1;
          BankTransfer bank_transfer = 2;
          DigitalWallet digital_wallet = 3;
        }
      }
      
      // Reserve removed field numbers to prevent reuse
      message Order {
        string id = 1;
        string customer_id = 2;
        // Field 3 was deprecated_field
        reserved 3;
        reserved "deprecated_field";
        OrderStatus status = 4;
      }
      
      // Use enums with explicit zero value as unknown/default
      enum OrderStatus {
        ORDER_STATUS_UNSPECIFIED = 0;
        ORDER_STATUS_PENDING = 1;
        ORDER_STATUS_CONFIRMED = 2;
        ORDER_STATUS_SHIPPED = 3;
        ORDER_STATUS_DELIVERED = 4;
        ORDER_STATUS_CANCELLED = 5;
      }
      ```
      
      ### Pagination Pattern
      
      ```protobuf
      message ListOrdersRequest {
        int32 page_size = 1;
        string page_token = 2;
        OrderFilter filter = 3;
      }
      
      message ListOrdersResponse {
        repeated Order orders = 1;
        string next_page_token = 2;
        int32 total_count = 3;
      }
      ```
      
      ## Streaming Patterns
      
      ### Server Streaming with Backpressure
      
      ```csharp
      public override async Task StreamOrders(
          ListOrdersRequest request,
          IServerStreamWriter<Order> responseStream,
          ServerCallContext context)
      {
          var batchSize = 100;
          var lastId = string.Empty;
      
          while (!context.CancellationToken.IsCancellationRequested)
          {
              var orders = await _repository.GetOrdersBatchAsync(
                  lastId,
                  batchSize,
                  context.CancellationToken);
      
              if (orders.Count == 0)
                  break;
      
              foreach (var order in orders)
              {
                  // WriteAsync handles backpressure automatically
                  await responseStream.WriteAsync(order, context.CancellationToken);
                  lastId = order.Id;
              }
      
              // Optional: yield control to prevent starvation
              await Task.Yield();
          }
      }
      ```
      
      ### Client Streaming with Batching
      
      ```csharp
      public override async Task<UploadOrdersResponse> UploadOrders(
          IAsyncStreamReader<CreateOrderRequest> requestStream,
          ServerCallContext context)
      {
          var batch = new List<CreateOrderRequest>();
          var batchSize = 100;
          var totalProcessed = 0;
          var errors = new List<string>();
      
          await foreach (var request in requestStream.ReadAllAsync(context.CancellationToken))
          {
              batch.Add(request);
      
              if (batch.Count >= batchSize)
              {
                  var result = await ProcessBatchAsync(batch, context.CancellationToken);
                  totalProcessed += result.SuccessCount;
                  errors.AddRange(result.Errors);
                  batch.Clear();
              }
          }
      
          // Process remaining items
          if (batch.Count > 0)
          {
              var result = await ProcessBatchAsync(batch, context.CancellationToken);
              totalProcessed += result.SuccessCount;
              errors.AddRange(result.Errors);
          }
      
          return new UploadOrdersResponse
          {
              ProcessedCount = totalProcessed,
              ErrorCount = errors.Count,
              Errors = { errors.Take(10) }
          };
      }
      ```
      
      ### Bidirectional Streaming with Concurrent Processing
      
      ```csharp
      public override async Task SyncOrders(
          IAsyncStreamReader<OrderSyncMessage> requestStream,
          IServerStreamWriter<OrderSyncMessage> responseStream,
          ServerCallContext context)
      {
          var pendingResponses = Channel.CreateUnbounded<OrderSyncMessage>();
      
          // Writer task - sends responses as they become available
          var writerTask = Task.Run(async () =>
          {
              await foreach (var response in pendingResponses.Reader.ReadAllAsync(context.CancellationToken))
              {
                  await responseStream.WriteAsync(response, context.CancellationToken);
              }
          }, context.CancellationToken);
      
          // Reader task - processes incoming messages concurrently
          try
          {
              await foreach (var message in requestStream.ReadAllAsync(context.CancellationToken))
              {
                  // Process each message asynchronously
                  _ = ProcessAndQueueResponseAsync(message, pendingResponses.Writer, context.CancellationToken);
              }
          }
          finally
          {
              pendingResponses.Writer.Complete();
              await writerTask;
          }
      }
      
      private async Task ProcessAndQueueResponseAsync(
          OrderSyncMessage message,
          ChannelWriter<OrderSyncMessage> writer,
          CancellationToken ct)
      {
          try
          {
              var response = await ProcessSyncMessageAsync(message, ct);
              await writer.WriteAsync(response, ct);
          }
          catch (Exception ex)
          {
              await writer.WriteAsync(new OrderSyncMessage
              {
                  CorrelationId = message.CorrelationId,
                  Error = ex.Message
              }, ct);
          }
      }
      ```
      
      ### Streaming with Heartbeats
      
      ```csharp
      public override async Task Subscribe(
          SubscribeRequest request,
          IServerStreamWriter<Event> responseStream,
          ServerCallContext context)
      {
          var subscription = await _eventBus.SubscribeAsync(request.Topic, context.CancellationToken);
          var heartbeatInterval = TimeSpan.FromSeconds(30);
          var lastActivity = DateTime.UtcNow;
      
          using var heartbeatTimer = new PeriodicTimer(heartbeatInterval);
      
          // Start heartbeat task
          var heartbeatTask = Task.Run(async () =>
          {
              while (await heartbeatTimer.WaitForNextTickAsync(context.CancellationToken))
              {
                  if (DateTime.UtcNow - lastActivity > heartbeatInterval)
                  {
                      await responseStream.WriteAsync(new Event { IsHeartbeat = true }, context.CancellationToken);
                  }
              }
          }, context.CancellationToken);
      
          try
          {
              await foreach (var evt in subscription.ReadAllAsync(context.CancellationToken))
              {
                  await responseStream.WriteAsync(evt, context.CancellationToken);
                  lastActivity = DateTime.UtcNow;
              }
          }
          finally
          {
              await heartbeatTask;
          }
      }
      ```
      
      ## Interceptor Patterns
      
      ### Authentication Interceptor (Server)
      
      ```csharp
      public class AuthenticationInterceptor : Interceptor
      {
          private readonly ITokenValidator _tokenValidator;
          private readonly ILogger<AuthenticationInterceptor> _logger;
      
          // Methods that don't require authentication
          private static readonly HashSet<string> AnonymousMethods = new(StringComparer.OrdinalIgnoreCase)
          {
              "/grpc.health.v1.Health/Check",
              "/mycompany.auth.v1.AuthService/Login"
          };
      
          public AuthenticationInterceptor(
              ITokenValidator tokenValidator,
              ILogger<AuthenticationInterceptor> logger)
          {
              _tokenValidator = tokenValidator;
              _logger = logger;
          }
      
          public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
              TRequest request,
              ServerCallContext context,
              UnaryServerMethod<TRequest, TResponse> continuation)
          {
              await ValidateAuthenticationAsync(context);
              return await continuation(request, context);
          }
      
          public override async Task ServerStreamingServerHandler<TRequest, TResponse>(
              TRequest request,
              IServerStreamWriter<TResponse> responseStream,
              ServerCallContext context,
              ServerStreamingServerMethod<TRequest, TResponse> continuation)
          {
              await ValidateAuthenticationAsync(context);
              await continuation(request, responseStream, context);
          }
      
          private async Task ValidateAuthenticationAsync(ServerCallContext context)
          {
              if (AnonymousMethods.Contains(context.Method))
                  return;
      
              var authHeader = context.RequestHeaders.GetValue("authorization");
              if (string.IsNullOrEmpty(authHeader))
              {
                  throw new RpcException(new Status(StatusCode.Unauthenticated, "Missing authorization header"));
              }
      
              if (!authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
              {
                  throw new RpcException(new Status(StatusCode.Unauthenticated, "Invalid authorization scheme"));
              }
      
              var token = authHeader.Substring(7);
              var principal = await _tokenValidator.ValidateTokenAsync(token, context.CancellationToken);
      
              if (principal == null)
              {
                  throw new RpcException(new Status(StatusCode.Unauthenticated, "Invalid or expired token"));
              }
      
              // Store principal for downstream use
              context.UserState["ClaimsPrincipal"] = principal;
          }
      }
      ```
      
      ### Authentication Interceptor (Client)
      
      ```csharp
      public class ClientAuthInterceptor : Interceptor
      {
          private readonly ITokenProvider _tokenProvider;
      
          public ClientAuthInterceptor(ITokenProvider tokenProvider)
          {
              _tokenProvider = tokenProvider;
          }
      
          public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
              TRequest request,
              ClientInterceptorContext<TRequest, TResponse> context,
              AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
          {
              var newContext = AddAuthHeader(context);
              return continuation(request, newContext);
          }
      
          public override AsyncServerStreamingCall<TResponse> AsyncServerStreamingCall<TRequest, TResponse>(
              TRequest request,
              ClientInterceptorContext<TRequest, TResponse> context,
              AsyncServerStreamingCallContinuation<TRequest, TResponse> continuation)
          {
              var newContext = AddAuthHeader(context);
              return continuation(request, newContext);
          }
      
          private ClientInterceptorContext<TRequest, TResponse> AddAuthHeader<TRequest, TResponse>(
              ClientInterceptorContext<TRequest, TResponse> context)
              where TRequest : class
              where TResponse : class
          {
              var token = _tokenProvider.GetToken();
              if (string.IsNullOrEmpty(token))
                  return context;
      
              var headers = context.Options.Headers ?? new Metadata();
              headers.Add("authorization", $"Bearer {token}");
      
              return new ClientInterceptorContext<TRequest, TResponse>(
                  context.Method,
                  context.Host,
                  context.Options.WithHeaders(headers));
          }
      }
      ```
      
      ### Retry Interceptor with Circuit Breaker
      
      ```csharp
      public class RetryInterceptor : Interceptor
      {
          private readonly ILogger<RetryInterceptor> _logger;
          private readonly RetryPolicy _policy;
      
          private static readonly HashSet<StatusCode> RetryableStatusCodes = new()
          {
              StatusCode.Unavailable,
              StatusCode.Aborted,
              StatusCode.DeadlineExceeded,
              StatusCode.ResourceExhausted
          };
      
          public RetryInterceptor(ILogger<RetryInterceptor> logger, RetryPolicy policy)
          {
              _logger = logger;
              _policy = policy;
          }
      
          public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
              TRequest request,
              ClientInterceptorContext<TRequest, TResponse> context,
              AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
          {
              var call = continuation(request, context);
      
              return new AsyncUnaryCall<TResponse>(
                  RetryAsync(call.ResponseAsync, request, context, continuation),
                  call.ResponseHeadersAsync,
                  call.GetStatus,
                  call.GetTrailers,
                  call.Dispose);
          }
      
          private async Task<TResponse> RetryAsync<TRequest, TResponse>(
              Task<TResponse> responseTask,
              TRequest request,
              ClientInterceptorContext<TRequest, TResponse> context,
              AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
              where TRequest : class
              where TResponse : class
          {
              var attempt = 0;
              var delay = _policy.InitialBackoff;
      
              while (true)
              {
                  try
                  {
                      return await responseTask;
                  }
                  catch (RpcException ex) when (ShouldRetry(ex, attempt))
                  {
                      attempt++;
                      _logger.LogWarning(
                          "Retry attempt {Attempt} for {Method} after {Status}",
                          attempt, context.Method.FullName, ex.StatusCode);
      
                      await Task.Delay(delay);
                      delay = TimeSpan.FromTicks(Math.Min(
                          (long)(delay.Ticks * _policy.BackoffMultiplier),
                          _policy.MaxBackoff.Ticks));
      
                      var call = continuation(request, context);
                      responseTask = call.ResponseAsync;
                  }
              }
          }
      
          private bool ShouldRetry(RpcException ex, int attempt)
          {
              return attempt < _policy.MaxAttempts
                  && RetryableStatusCodes.Contains(ex.StatusCode);
          }
      }
      
      public record RetryPolicy(
          int MaxAttempts = 3,
          TimeSpan InitialBackoff = default,
          TimeSpan MaxBackoff = default,
          double BackoffMultiplier = 2.0)
      {
          public TimeSpan InitialBackoff { get; init; } = InitialBackoff == default
              ? TimeSpan.FromMilliseconds(100)
              : InitialBackoff;
      
          public TimeSpan MaxBackoff { get; init; } = MaxBackoff == default
              ? TimeSpan.FromSeconds(5)
              : MaxBackoff;
      }
      ```
      
      ### Validation Interceptor
      
      ```csharp
      public class ValidationInterceptor : Interceptor
      {
          private readonly IServiceProvider _serviceProvider;
      
          public ValidationInterceptor(IServiceProvider serviceProvider)
          {
              _serviceProvider = serviceProvider;
          }
      
          public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
              TRequest request,
              ServerCallContext context,
              UnaryServerMethod<TRequest, TResponse> continuation)
          {
              await ValidateRequestAsync(request, context.CancellationToken);
              return await continuation(request, context);
          }
      
          private async Task ValidateRequestAsync<TRequest>(TRequest request, CancellationToken ct)
          {
              var validator = _serviceProvider.GetService<IValidator<TRequest>>();
              if (validator == null)
                  return;
      
              var result = await validator.ValidateAsync(request, ct);
              if (!result.IsValid)
              {
                  var errors = string.Join("; ", result.Errors.Select(e => e.ErrorMessage));
                  throw new RpcException(new Status(StatusCode.InvalidArgument, errors));
              }
          }
      }
      ```
      
      ### Metrics Interceptor
      
      ```csharp
      public class MetricsInterceptor : Interceptor
      {
          private readonly IMeterFactory _meterFactory;
          private readonly Histogram<double> _requestDuration;
          private readonly Counter<long> _requestCount;
      
          public MetricsInterceptor(IMeterFactory meterFactory)
          {
              _meterFactory = meterFactory;
              var meter = _meterFactory.Create("GrpcServer");
      
              _requestDuration = meter.CreateHistogram<double>(
                  "grpc.server.request.duration",
                  unit: "ms",
                  description: "Duration of gRPC requests");
      
              _requestCount = meter.CreateCounter<long>(
                  "grpc.server.request.count",
                  description: "Number of gRPC requests");
          }
      
          public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
              TRequest request,
              ServerCallContext context,
              UnaryServerMethod<TRequest, TResponse> continuation)
          {
              var sw = Stopwatch.StartNew();
              var status = StatusCode.OK;
      
              try
              {
                  return await continuation(request, context);
              }
              catch (RpcException ex)
              {
                  status = ex.StatusCode;
                  throw;
              }
              finally
              {
                  sw.Stop();
                  RecordMetrics(context.Method, status, sw.Elapsed.TotalMilliseconds);
              }
          }
      
          private void RecordMetrics(string method, StatusCode status, double duration)
          {
              var tags = new TagList
              {
                  { "grpc.method", method },
                  { "grpc.status_code", status.ToString() }
              };
      
              _requestDuration.Record(duration, tags);
              _requestCount.Add(1, tags);
          }
      }
      ```
      
      ## Deadline Propagation
      
      ```csharp
      public class DeadlinePropagationInterceptor : Interceptor
      {
          private readonly TimeSpan _bufferTime = TimeSpan.FromMilliseconds(100);
      
          public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
              TRequest request,
              ClientInterceptorContext<TRequest, TResponse> context,
              AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
          {
              var newContext = PropagateDeadline(context);
              return continuation(request, newContext);
          }
      
          private ClientInterceptorContext<TRequest, TResponse> PropagateDeadline<TRequest, TResponse>(
              ClientInterceptorContext<TRequest, TResponse> context)
              where TRequest : class
              where TResponse : class
          {
              // Check if there's an incoming deadline from the current call context
              if (ServerCallContext.Current?.Deadline is { } incomingDeadline)
              {
                  var remaining = incomingDeadline - DateTime.UtcNow;
      
                  // Apply buffer to leave time for response processing
                  var adjustedRemaining = remaining - _bufferTime;
      
                  if (adjustedRemaining > TimeSpan.Zero)
                  {
                      var newOptions = context.Options.WithDeadline(DateTime.UtcNow + adjustedRemaining);
                      return new ClientInterceptorContext<TRequest, TResponse>(
                          context.Method,
                          context.Host,
                          newOptions);
                  }
              }
      
              return context;
          }
      }
      ```
      
      ## Health Check Implementation
      
      ```csharp
      public class HealthServiceImpl : Health.HealthBase
      {
          private readonly IEnumerable<IHealthCheck> _healthChecks;
          private readonly ConcurrentDictionary<string, HealthCheckResponse.Types.ServingStatus> _statusMap = new();
      
          public HealthServiceImpl(IEnumerable<IHealthCheck> healthChecks)
          {
              _healthChecks = healthChecks;
          }
      
          public override async Task<HealthCheckResponse> Check(
              HealthCheckRequest request,
              ServerCallContext context)
          {
              var service = request.Service;
      
              if (string.IsNullOrEmpty(service))
              {
                  // Overall health check
                  var allHealthy = await CheckAllServicesAsync(context.CancellationToken);
                  return new HealthCheckResponse
                  {
                      Status = allHealthy
                          ? HealthCheckResponse.Types.ServingStatus.Serving
                          : HealthCheckResponse.Types.ServingStatus.NotServing
                  };
              }
      
              if (_statusMap.TryGetValue(service, out var status))
              {
                  return new HealthCheckResponse { Status = status };
              }
      
              throw new RpcException(new Status(StatusCode.NotFound, $"Service '{service}' not found"));
          }
      
          public override async Task Watch(
              HealthCheckRequest request,
              IServerStreamWriter<HealthCheckResponse> responseStream,
              ServerCallContext context)
          {
              var lastStatus = HealthCheckResponse.Types.ServingStatus.Unknown;
      
              while (!context.CancellationToken.IsCancellationRequested)
              {
                  var currentStatus = await GetServiceStatusAsync(request.Service, context.CancellationToken);
      
                  if (currentStatus != lastStatus)
                  {
                      await responseStream.WriteAsync(new HealthCheckResponse { Status = currentStatus });
                      lastStatus = currentStatus;
                  }
      
                  await Task.Delay(TimeSpan.FromSeconds(5), context.CancellationToken);
              }
          }
      
          private async Task<bool> CheckAllServicesAsync(CancellationToken ct)
          {
              foreach (var check in _healthChecks)
              {
                  var result = await check.CheckHealthAsync(new HealthCheckContext(), ct);
                  if (result.Status != HealthStatus.Healthy)
                      return false;
              }
              return true;
          }
      
          private async Task<HealthCheckResponse.Types.ServingStatus> GetServiceStatusAsync(
              string service,
              CancellationToken ct)
          {
              // Implementation depends on service-specific health checks
              return HealthCheckResponse.Types.ServingStatus.Serving;
          }
      }
      ```
      
      ## Load Balancing Client Configuration
      
      ```csharp
      // Configure DNS-based load balancing
      var channel = GrpcChannel.ForAddress("dns:///my-service.example.com", new GrpcChannelOptions
      {
          ServiceConfig = new ServiceConfig
          {
              LoadBalancingConfigs = { new RoundRobinConfig() },
              MethodConfigs =
              {
                  new MethodConfig
                  {
                      Names = { MethodName.Default },
                      RetryPolicy = new RetryPolicy
                      {
                          MaxAttempts = 3,
                          InitialBackoff = TimeSpan.FromMilliseconds(100),
                          MaxBackoff = TimeSpan.FromSeconds(2),
                          BackoffMultiplier = 1.5,
                          RetryableStatusCodes = { StatusCode.Unavailable }
                      }
                  }
              }
          },
          Credentials = ChannelCredentials.SecureSsl
      });
      ```
      
      ## Compression Configuration
      
      ```csharp
      // Server-side compression
      builder.Services.AddGrpc(options =>
      {
          options.ResponseCompressionAlgorithm = "gzip";
          options.ResponseCompressionLevel = CompressionLevel.Optimal;
          options.CompressionProviders = new List<ICompressionProvider>
          {
              new GzipCompressionProvider(CompressionLevel.Optimal)
          };
      });
      
      // Client-side compression
      var callOptions = new CallOptions(
          headers: new Metadata
          {
              { "grpc-accept-encoding", "gzip" }
          },
          writeOptions: new WriteOptions(WriteFlags.NoCompress));
      ```
      
  • SKILL.md 14.5 KB
    ---
    name: dotnet-grpc
    version: "1.0.0"
    category: "Web"
    description: "Build or review gRPC services and clients in .NET with correct contract-first design, streaming behavior, transport assumptions, and backend service integration."
    compatibility: "Requires ASP.NET Core gRPC or gRPC client projects."
    ---
    
    # gRPC for .NET
    
    ## Trigger On
    
    - building backend-to-backend RPC services or clients
    - adding protobuf contracts, streaming calls, or interceptors
    - deciding between gRPC, HTTP APIs, and SignalR
    - optimizing gRPC performance and connection management
    - implementing service-to-service communication in microservices
    
    ## Documentation
    
    - [gRPC on .NET Overview](https://learn.microsoft.com/en-us/aspnet/core/grpc/?view=aspnetcore-10.0)
    - [Performance Best Practices with gRPC](https://learn.microsoft.com/en-us/aspnet/core/grpc/performance?view=aspnetcore-10.0)
    - [gRPC Client Factory](https://learn.microsoft.com/en-us/aspnet/core/grpc/clientfactory?view=aspnetcore-10.0)
    - [gRPC Interceptors](https://learn.microsoft.com/en-us/aspnet/core/grpc/interceptors?view=aspnetcore-10.0)
    - [Call gRPC Services with .NET Client](https://learn.microsoft.com/en-us/aspnet/core/grpc/client?view=aspnetcore-10.0)
    
    ### References
    
    - [patterns.md](references/patterns.md) - Detailed proto patterns, streaming implementations, interceptors, health checks, and load balancing
    - [anti-patterns.md](references/anti-patterns.md) - Common gRPC mistakes with explanations and corrections
    
    ## Workflow
    
    1. Use gRPC where low-latency backend communication, strong contracts, or streaming are the real drivers.
    2. Treat `.proto` files as source of truth and keep generated code ownership clear.
    3. Choose unary, server streaming, client streaming, or bidirectional streaming based on the interaction model, not by default.
    4. Do not use gRPC for broad browser-facing APIs unless the limitations and gRPC-Web tradeoffs are explicitly acceptable.
    5. Handle deadlines, cancellation, auth, and retry behavior explicitly on both server and client paths.
    6. Validate contract changes carefully because gRPC drift breaks callers fast.
    
    ## Service Patterns
    
    ### Basic Unary Service
    ```csharp
    // greeter.proto
    syntax = "proto3";
    
    option csharp_namespace = "GrpcService";
    
    package greet;
    
    service Greeter {
      rpc SayHello (HelloRequest) returns (HelloReply);
    }
    
    message HelloRequest {
      string name = 1;
    }
    
    message HelloReply {
      string message = 1;
    }
    ```
    
    ```csharp
    // GreeterService.cs
    public class GreeterService : Greeter.GreeterBase
    {
        private readonly ILogger<GreeterService> _logger;
    
        public GreeterService(ILogger<GreeterService> logger)
        {
            _logger = logger;
        }
    
        public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
        {
            _logger.LogInformation("Greeting {Name}", request.Name);
            return Task.FromResult(new HelloReply
            {
                Message = $"Hello {request.Name}"
            });
        }
    }
    ```
    
    ### Server Streaming
    ```csharp
    // In .proto file
    service DataStream {
      rpc StreamData (DataRequest) returns (stream DataChunk);
    }
    
    // Service implementation
    public override async Task StreamData(
        DataRequest request,
        IServerStreamWriter<DataChunk> responseStream,
        ServerCallContext context)
    {
        for (int i = 0; i < request.Count; i++)
        {
            // Check for cancellation to avoid wasted work
            if (context.CancellationToken.IsCancellationRequested)
            {
                _logger.LogInformation("Stream cancelled by client");
                break;
            }
    
            await responseStream.WriteAsync(new DataChunk
            {
                Index = i,
                Data = await GetDataAsync(i)
            });
    
            // Respect backpressure
            await Task.Delay(10, context.CancellationToken);
        }
    }
    ```
    
    ### Bidirectional Streaming
    ```csharp
    // In .proto file
    service Chat {
      rpc ChatStream (stream ChatMessage) returns (stream ChatMessage);
    }
    
    // Service implementation
    public override async Task ChatStream(
        IAsyncStreamReader<ChatMessage> requestStream,
        IServerStreamWriter<ChatMessage> responseStream,
        ServerCallContext context)
    {
        await foreach (var message in requestStream.ReadAllAsync(context.CancellationToken))
        {
            _logger.LogInformation("Received: {Message}", message.Text);
    
            // Echo back with transformation
            await responseStream.WriteAsync(new ChatMessage
            {
                Text = $"Echo: {message.Text}",
                Timestamp = Timestamp.FromDateTime(DateTime.UtcNow)
            });
        }
    }
    ```
    
    ## Client Patterns
    
    ### Channel Reuse with Client Factory (Recommended)
    ```csharp
    // Program.cs - Register gRPC client with factory
    builder.Services.AddGrpcClient<Greeter.GreeterClient>(options =>
    {
        options.Address = new Uri("https://localhost:5001");
    })
    .ConfigurePrimaryHttpMessageHandler(() =>
    {
        var handler = new SocketsHttpHandler
        {
            PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
            KeepAlivePingDelay = TimeSpan.FromSeconds(60),
            KeepAlivePingTimeout = TimeSpan.FromSeconds(30),
            EnableMultipleHttp2Connections = true
        };
        return handler;
    })
    .AddInterceptor<LoggingInterceptor>();
    
    // Usage in service
    public class MyService
    {
        private readonly Greeter.GreeterClient _client;
    
        public MyService(Greeter.GreeterClient client)
        {
            _client = client;
        }
    
        public async Task<string> GreetAsync(string name, CancellationToken ct)
        {
            // Always set deadlines
            var deadline = DateTime.UtcNow.AddSeconds(5);
            var response = await _client.SayHelloAsync(
                new HelloRequest { Name = name },
                deadline: deadline,
                cancellationToken: ct);
            return response.Message;
        }
    }
    ```
    
    ### Manual Channel Creation with Connection Options
    ```csharp
    // Reuse channels - expensive to create
    var channel = GrpcChannel.ForAddress("https://localhost:5001", new GrpcChannelOptions
    {
        HttpHandler = new SocketsHttpHandler
        {
            EnableMultipleHttp2Connections = true,
            PooledConnectionIdleTimeout = Timeout.InfiniteTimeSpan,
            KeepAlivePingDelay = TimeSpan.FromSeconds(60),
            KeepAlivePingTimeout = TimeSpan.FromSeconds(30)
        },
        MaxRetryAttempts = 3,
        ServiceConfig = new ServiceConfig
        {
            MethodConfigs =
            {
                new MethodConfig
                {
                    Names = { MethodName.Default },
                    RetryPolicy = new RetryPolicy
                    {
                        MaxAttempts = 3,
                        InitialBackoff = TimeSpan.FromSeconds(1),
                        MaxBackoff = TimeSpan.FromSeconds(5),
                        BackoffMultiplier = 1.5,
                        RetryableStatusCodes = { StatusCode.Unavailable }
                    }
                }
            }
        }
    });
    
    // Create multiple clients from same channel
    var greeterClient = new Greeter.GreeterClient(channel);
    var orderClient = new Orders.OrdersClient(channel);
    ```
    
    ### Consuming Server Streaming
    ```csharp
    public async Task ProcessStreamAsync(CancellationToken ct)
    {
        using var call = _client.StreamData(new DataRequest { Count = 100 });
    
        try
        {
            await foreach (var chunk in call.ResponseStream.ReadAllAsync(ct))
            {
                await ProcessChunkAsync(chunk);
            }
        }
        catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled)
        {
            _logger.LogInformation("Stream cancelled");
        }
    }
    ```
    
    ### Bidirectional Streaming Client
    ```csharp
    public async Task ChatAsync(CancellationToken ct)
    {
        using var call = _client.ChatStream();
    
        // Read responses in background
        var readTask = Task.Run(async () =>
        {
            await foreach (var response in call.ResponseStream.ReadAllAsync(ct))
            {
                Console.WriteLine($"Received: {response.Text}");
            }
        }, ct);
    
        // Send messages
        foreach (var message in GetMessages())
        {
            if (ct.IsCancellationRequested) break;
    
            await call.RequestStream.WriteAsync(new ChatMessage { Text = message });
        }
    
        // Signal completion and wait for responses
        await call.RequestStream.CompleteAsync();
        await readTask;
    }
    ```
    
    ## Interceptor Patterns
    
    ### Logging Interceptor
    ```csharp
    public class LoggingInterceptor : Interceptor
    {
        private readonly ILogger<LoggingInterceptor> _logger;
    
        public LoggingInterceptor(ILogger<LoggingInterceptor> logger)
        {
            _logger = logger;
        }
    
        public override AsyncUnaryCall<TResponse> AsyncUnaryCall<TRequest, TResponse>(
            TRequest request,
            ClientInterceptorContext<TRequest, TResponse> context,
            AsyncUnaryCallContinuation<TRequest, TResponse> continuation)
        {
            var sw = Stopwatch.StartNew();
            var call = continuation(request, context);
    
            return new AsyncUnaryCall<TResponse>(
                HandleResponse(call.ResponseAsync, context.Method.FullName, sw),
                call.ResponseHeadersAsync,
                call.GetStatus,
                call.GetTrailers,
                call.Dispose);
        }
    
        private async Task<TResponse> HandleResponse<TResponse>(
            Task<TResponse> responseTask, string method, Stopwatch sw)
        {
            try
            {
                var response = await responseTask;
                _logger.LogInformation("{Method} completed in {Elapsed}ms",
                    method, sw.ElapsedMilliseconds);
                return response;
            }
            catch (RpcException ex)
            {
                _logger.LogError(ex, "{Method} failed with {Status} in {Elapsed}ms",
                    method, ex.StatusCode, sw.ElapsedMilliseconds);
                throw;
            }
        }
    }
    ```
    
    ### Server Exception Interceptor
    ```csharp
    public class ExceptionInterceptor : Interceptor
    {
        private readonly ILogger<ExceptionInterceptor> _logger;
    
        public ExceptionInterceptor(ILogger<ExceptionInterceptor> logger)
        {
            _logger = logger;
        }
    
        public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
            TRequest request,
            ServerCallContext context,
            UnaryServerMethod<TRequest, TResponse> continuation)
        {
            try
            {
                return await continuation(request, context);
            }
            catch (RpcException)
            {
                throw; // Let gRPC exceptions pass through
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Unhandled exception in {Method}", context.Method);
                throw new RpcException(new Status(StatusCode.Internal, "An error occurred"));
            }
        }
    }
    ```
    
    ## Server Configuration
    
    ```csharp
    var builder = WebApplication.CreateBuilder(args);
    
    builder.Services.AddGrpc(options =>
    {
        options.EnableDetailedErrors = builder.Environment.IsDevelopment();
        options.MaxReceiveMessageSize = 4 * 1024 * 1024; // 4 MB
        options.MaxSendMessageSize = 4 * 1024 * 1024;
        options.Interceptors.Add<ExceptionInterceptor>();
    });
    
    // Configure Kestrel for HTTP/2
    builder.WebHost.ConfigureKestrel(options =>
    {
        options.Limits.Http2.MaxStreamsPerConnection = 100;
        options.Limits.Http2.InitialConnectionWindowSize = 1024 * 1024; // 1 MB
        options.Limits.Http2.InitialStreamWindowSize = 512 * 1024; // 512 KB
    });
    
    var app = builder.Build();
    
    app.MapGrpcService<GreeterService>();
    app.MapGet("/", () => "gRPC endpoint");
    
    app.Run();
    ```
    
    ## Anti-Patterns to Avoid
    
    | Anti-Pattern | Why It's Bad | Better Approach |
    |--------------|--------------|-----------------|
    | Creating new channel per call | Connection overhead kills performance | Reuse channels, use client factory |
    | Missing deadlines | Calls can hang indefinitely | Always set deadline on client calls |
    | Ignoring cancellation in streams | Wastes server resources | Check `CancellationToken` periodically |
    | Using gRPC for browser clients | Limited browser support | Use gRPC-Web with Envoy or REST |
    | Large messages (>1MB) | Memory pressure, LOH allocations | Stream in chunks or use HTTP for files |
    | Sync blocking (`Task.Result`) | Thread pool starvation | Use async/await consistently |
    | Swallowing exceptions in interceptors | Hides failures from clients | Rethrow or convert to `RpcException` |
    | Not aligning client/server deadlines | Mismatched timeout behavior | Coordinate deadline budgets |
    | Blocking `AsyncUnaryCall` with `BlockingUnaryCall` interceptor | Interceptors are method-specific | Implement both interceptor methods |
    | Missing retry configuration | Single failures cause request failure | Configure retry policy on channel |
    
    ## Best Practices
    
    ### Channel and Connection Management
    1. **Reuse channels** across the application lifetime
    2. **Enable multiple HTTP/2 connections** with `EnableMultipleHttp2Connections = true`
    3. **Configure keep-alive pings** to maintain connections through idle periods
    4. **Use client factory** (`AddGrpcClient`) for centralized channel management
    5. **Set `PooledConnectionIdleTimeout`** to prevent premature connection closure
    
    ### Deadlines and Cancellation
    1. **Always set deadlines** on client calls to prevent indefinite hangs
    2. **Propagate cancellation** through the call chain
    3. **Check cancellation** in long-running streaming handlers
    4. **Coordinate deadline budgets** between client and server
    
    ### Performance
    1. **Avoid large messages** (>85KB to stay off Large Object Heap)
    2. **Use streaming** for large data transfers instead of single messages
    3. **Enable server GC** for high-throughput client applications
    4. **Complete streams gracefully** to allow connection reuse
    5. **Dispose streaming calls** when done to release resources
    
    ### Error Handling
    1. **Use appropriate status codes** (not just `Internal` for everything)
    2. **Let `RpcException` propagate** through interceptors
    3. **Convert domain exceptions** to gRPC status codes at service boundaries
    4. **Include meaningful error details** in development mode only
    
    ### Contract Design
    1. **Use custom objects** in proto to enable backward-compatible evolution
    2. **Reserve field numbers** you remove instead of reusing
    3. **Version service names** for breaking changes (`GreeterV2`)
    4. **Keep proto files** as the single source of truth
    
    ### Observability
    1. **Add logging interceptors** for request/response timing
    2. **Track error rates** by status code
    3. **Monitor connection pool** health and reuse rates
    4. **Integrate with distributed tracing** (OpenTelemetry)
    
    ## Deliver
    
    - stable protobuf contracts and generated code flow
    - service and client code that match the RPC shape
    - tests or smoke checks for serialization and call behavior
    - proper deadline and cancellation handling
    
    ## Validate
    
    - gRPC is chosen for the right problem
    - streaming semantics and deadlines are explicit
    - browser constraints are acknowledged when relevant
    - channels are reused appropriately
    - error handling converts exceptions to proper status codes
    - interceptors are ordered correctly (logging before auth before validation)
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related