pace GrpcServices.Services;
public class InventoryService : InventoryService.InventoryServiceBase
{
private readonly ILogger<InventoryService> _logger;
private readonly ConcurrentDictionary<string, ItemResponse> _inventory;
public InventoryService(ILogger<InventoryService> logger,
ConcurrentDictionary<string, ItemResponse> inventory)
{
_logger = logger;
_inventory = inventory;
}
// Unary RPC
public override Task<ItemResponse> GetItem(GetItemRequest request, ServerCallContext context)
{
context.CancellationToken.ThrowIfCancellationRequested();
if (_inventory.TryGetValue(request.Sku, out var item))
{
return Task.FromResult(item);
}
throw new RpcException(new Status(StatusCode.NotFound, $"Item {request.Sku} not found."));
}
// Server Streaming RPC
public override async Task StreamUpdates(StreamRequest request, IServerStreamWriter<ItemUpdate> responseStream, ServerCallContext context)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(context.CancellationToken);
// Simulate background updates
while (!cts.Token.IsCancellationRequested)
{
foreach (var sku in request.WatchedSkus)
{
if (_inventory.TryGetValue(sku, out var current))
{
var update = new ItemUpdate
{
Sku = sku,
NewQuantity = current.Quantity,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
};
await responseStream.WriteAsync(update);
}
}
await Task.Delay(1000, cts.Token);
}
}
// Client Streaming RPC
public override async Task<UpdateSummary> BulkUpdate(IAsyncStreamReader<BulkItem> requestStream, ServerCallContext context)
{
int processed = 0;
int errors = 0;
await foreach (var item in requestStream.ReadAllAsync(context.CancellationToken))
{
try
{
if (_inventory.TryGetValue(item.Sku, out var existing))
{
var updated = new ItemResponse
{
Sku = existing.Sku,
Name = existing.Name,
Quantity = Math.Max(0, existing.Quantity + item.QuantityChange),
Tags = { existing.Tags }
};
_inventory[item.Sku] = updated;
processed++;
}
else
{
errors++;
}
}
catch
{
errors++;
}
}
return new UpdateSummary { ItemsProcessed = processed, Errors = errors };
}
}
#### 3. Server Configuration
Kestrel must be configured to support HTTP/2. For production, TLS is mandatory for HTTP/2 in most environments.
```csharp
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddGrpc(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
options.MaxReceiveMessageSize = 64 * 1024 * 1024; // 64MB
options.MaxSendMessageSize = 64 * 1024 * 1024;
});
// Add interceptors for cross-cutting concerns
builder.Services.AddTransient<LoggingInterceptor>();
builder.Services.AddTransient<AuthInterceptor>();
var app = builder.Build();
app.MapGrpcService<InventoryService>();
app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2099682");
app.Run();
4. Client Implementation
Clients use Grpc.Net.Client. Deadlines should be enforced to prevent resource leaks.
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new InventoryService.InventoryServiceClient(channel);
// Unary call with deadline
try
{
var reply = await client.GetItemAsync(
new GetItemRequest { Sku = "SKU-123" },
deadline: DateTime.UtcNow.AddSeconds(5)
);
Console.WriteLine($"Item: {reply.Name}, Qty: {reply.Quantity}");
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
Console.WriteLine("Request timed out.");
}
Architecture Rationale
- Code Generation: Eliminates manual serialization logic and ensures consistency.
- HTTP/2: Multiplexing reduces connection overhead and improves latency under load.
- Streaming: Enables efficient real-time data flow without polling, reducing server load.
- Interceptors: Centralize authentication, logging, and metrics, keeping service logic clean.
Pitfall Guide
1. Ignoring HTTP/2 Negotiation Requirements
Mistake: Deploying gRPC services without proper HTTP/2 support or TLS configuration.
Impact: Clients fail to connect with StatusCode.Unavailable or protocol errors. Browsers and proxies may downgrade to HTTP/1.1, breaking gRPC.
Best Practice: Ensure Kestrel is configured for HTTP/2. Use ALPN (Application-Layer Protocol Negotiation) for TLS. For environments that strip TLS (like some load balancers), use GrpcWeb or configure the load balancer to pass HTTP/2.
2. Improper Error Handling
Mistake: Throwing standard .NET exceptions or returning HTTP status codes manually.
Impact: Clients receive generic errors, losing semantic meaning. Stack traces may leak in production.
Best Practice: Always throw Grpc.Core.RpcException with appropriate StatusCode (e.g., NotFound, InvalidArgument, Internal). Use EnableDetailedErrors only in development. Map business exceptions to gRPC statuses via interceptors.
3. Protobuf Field Number Mutations
Mistake: Changing field numbers or removing fields in existing .proto files.
Impact: Data corruption and deserialization failures for clients using older versions.
Best Practice: Field numbers are immutable once deployed. To remove a field, reserve the number. Use optional for backward-compatible additions. Version your proto packages.
4. Missing Cancellation Tokens and Deadlines
Mistake: Implementing long-running operations without checking ServerCallContext.CancellationToken or setting client deadlines.
Impact: Server resources are consumed by abandoned requests, leading to memory leaks and thread pool exhaustion.
Best Practice: Always pass context.CancellationToken to async operations. Set deadlines on all client calls. Use context.WriteDeadline to inform clients of server-side timeouts.
5. Overusing Streaming for Simple Requests
Mistake: Using server streaming for single-response scenarios to "future-proof" the API.
Impact: Increased complexity for clients, higher overhead for single messages, and misuse of HTTP/2 streams.
Best Practice: Use unary RPCs for request/response patterns. Reserve streaming for event feeds, bulk processing, or real-time updates where the response cardinality is N.
6. Debugging Binary Protocols
Mistake: Attempting to debug gRPC traffic using standard HTTP tools or browser dev tools.
Impact: Inability to inspect payloads, leading to wasted troubleshooting time.
Best Practice: Use gRPC-specific tools like BloomRPC, Bloomin, or gRPCurl. Enable Grpc.AspNetCore logging to capture metadata. Use Wireshark with HTTP/2 decoding for network-level analysis.
7. Browser Compatibility Assumptions
Mistake: Assuming gRPC works natively in web browsers.
Impact: JavaScript clients cannot connect to standard gRPC endpoints due to HTTP/2 restrictions in browsers.
Best Practice: Implement GrpcWeb for browser clients. Configure the server to support both gRPC and gRPC-Web endpoints. Use Grpc.Net.Client.Web on the client side.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal Microservices | gRPC (Unary/Streaming) | Low latency, high throughput, strong typing, HTTP/2 efficiency. | Reduces bandwidth and compute costs; lowers latency. |
| Public API for Third Parties | REST/JSON | Easier integration, browser support, widespread tooling. | Higher bandwidth costs; slower development for consumers. |
| Browser Frontend Clients | gRPC-Web | Enables gRPC from browsers via HTTP/1.1 or HTTP/2 proxies. | Adds middleware overhead; requires dual-endpoint support. |
| High-Frequency IoT Telemetry | gRPC Client Streaming | Efficient bulk ingestion, reduced connection overhead. | Significantly lower ingestion costs; scalable architecture. |
| Rapid Prototyping / MVP | REST/JSON | No code generation, immediate debugging, flexible schema. | Technical debt if performance becomes critical later. |
| Mobile Apps | gRPC | Battery efficiency, smaller payloads, background streaming. | Better user experience; reduced data usage for users. |
Configuration Template
appsettings.json
{
"Kestrel": {
"Endpoints": {
"Https": {
"Url": "https://*:5001",
"Protocols": "Http2"
},
"Http": {
"Url": "http://*:5000",
"Protocols": "Http1AndHttp2"
}
}
},
"Grpc": {
"EnableDetailedErrors": false,
"MaxReceiveMessageSize": 67108864,
"MaxSendMessageSize": 67108864
}
}
Program.cs Snippet for gRPC-Web Support
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
});
builder.Services.AddGrpcWeb(o => o.GrpcWebEnabled = true);
var app = builder.Build();
app.UseGrpcWeb();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<InventoryService>().EnableGrpcWeb();
endpoints.MapGet("/", () => "gRPC services ready.");
});
app.Run();
Quick Start Guide
-
Create Project:
dotnet new grpc -n GrpcInventoryService
cd GrpcInventoryService
-
Define Proto:
Replace Protos/greet.proto with your service definition. Ensure csharp_namespace matches your project structure.
-
Build and Run:
dotnet build
dotnet run
The server starts with HTTP/2 enabled. Verify the endpoint in the console output.
-
Test with gRPCurl:
grpcurl -plaintext -d '{"name": "Codcompass"}' localhost:5000 greet.Greeter/SayHello
Note: Use -plaintext for local development without TLS. In production, omit this flag and provide certificates.
-
Implement Service:
Inherit from the generated base class in Services/GreeterService.cs. Implement methods and inject dependencies via constructor. Restart the server to apply changes.
This article provides the technical foundation, architectural context, and production-ready patterns required to implement ASP.NET Core gRPC services effectively. Adherence to contract-first design, HTTP/2 configuration, and rigorous error handling ensures the performance benefits of gRPC are realized without compromising maintainability or reliability.