Install
$ agentstack add skill-postpartum-genushyacinthus29-dotnet-skills-dotnet-grpc ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
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
- Performance Best Practices with gRPC
- gRPC Client Factory
- gRPC Interceptors
- Call gRPC Services with .NET Client
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
- Use gRPC where low-latency backend communication, strong contracts, or streaming are the real drivers.
- Treat
.protofiles as source of truth and keep generated code ownership clear. - Choose unary, server streaming, client streaming, or bidirectional streaming based on the interaction model, not by default.
- Do not use gRPC for broad browser-facing APIs unless the limitations and gRPC-Web tradeoffs are explicitly acceptable.
- Handle deadlines, cancellation, auth, and retry behavior explicitly on both server and client paths.
- 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 _logger;
public GreeterService(ILogger logger)
{
_logger = logger;
}
public override Task 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 responseStream,
ServerCallContext context)
{
for (int i = 0; i requestStream,
IServerStreamWriter 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(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();
// Usage in service
public class MyService
{
private readonly Greeter.GreeterClient _client;
public MyService(Greeter.GreeterClient client)
{
_client = client;
}
public async Task 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 _logger;
public LoggingInterceptor(ILogger logger)
{
_logger = logger;
}
public override AsyncUnaryCall AsyncUnaryCall(
TRequest request,
ClientInterceptorContext context,
AsyncUnaryCallContinuation continuation)
{
var sw = Stopwatch.StartNew();
var call = continuation(request, context);
return new AsyncUnaryCall(
HandleResponse(call.ResponseAsync, context.Method.FullName, sw),
call.ResponseHeadersAsync,
call.GetStatus,
call.GetTrailers,
call.Dispose);
}
private async Task HandleResponse(
Task 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 _logger;
public ExceptionInterceptor(ILogger logger)
{
_logger = logger;
}
public override async Task UnaryServerHandler(
TRequest request,
ServerCallContext context,
UnaryServerMethod 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();
});
// 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();
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
- Reuse channels across the application lifetime
- Enable multiple HTTP/2 connections with
EnableMultipleHttp2Connections = true - Configure keep-alive pings to maintain connections through idle periods
- Use client factory (
AddGrpcClient) for centralized channel management - Set
PooledConnectionIdleTimeoutto prevent premature connection closure
Deadlines and Cancellation
- Always set deadlines on client calls to prevent indefinite hangs
- Propagate cancellation through the call chain
- Check cancellation in long-running streaming handlers
- Coordinate deadline budgets between client and server
Performance
- Avoid large messages (>85KB to stay off Large Object Heap)
- Use streaming for large data transfers instead of single messages
- Enable server GC for high-throughput client applications
- Complete streams gracefully to allow connection reuse
- Dispose streaming calls when done to release resources
Error Handling
- Use appropriate status codes (not just
Internalfor everything) - Let
RpcExceptionpropagate through interceptors - Convert domain exceptions to gRPC status codes at service boundaries
- Include meaningful error details in development mode only
Contract Design
- Use custom objects in proto to enable backward-compatible evolution
- Reserve field numbers you remove instead of reusing
- Version service names for breaking changes (
GreeterV2) - Keep proto files as the single source of truth
Observability
- Add logging interceptors for request/response timing
- Track error rates by status code
- Monitor connection pool health and reuse rates
- 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)
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Postpartum-genushyacinthus29
- Source: Postpartum-genushyacinthus29/dotnet-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.