.NET & C# Development · Lesson 146 of 229
gRPC in .NET — Protobuf, Services, and Streaming
gRPC in .NET — Protobuf, Services, and Streaming
gRPC is a high-performance RPC framework that uses HTTP/2 and Protocol Buffers (Protobuf). It is the standard communication protocol between microservices at Google, Netflix, and most large-scale .NET shops.
Why gRPC?
REST: human-readable JSON over HTTP/1.1 — great for public APIs and browsers
gRPC: binary Protobuf over HTTP/2 — great for service-to-service communication
gRPC advantages over REST:
- 5–10× smaller payload (binary vs JSON)
- Strongly typed contract (.proto file is the single source of truth)
- HTTP/2 multiplexing — multiple calls on one connection
- Bidirectional streaming — neither party needs to poll
- Auto-generated client stubs in any language (C#, Go, Python, Java)
- Built-in deadline/cancellation propagation
gRPC disadvantages:
- Not browser-native (use gRPC-Web or REST for browser clients)
- Binary format is not human-readable (harder to debug)
- Requires HTTP/2 (most load balancers support it, but check)Step 1: Define the Contract (.proto file)
// Protos/orders.proto
syntax = "proto3";
option csharp_namespace = "OrderService";
package orders;
// The service definition
service OrderService {
// Unary RPC — one request, one response
rpc GetOrder (GetOrderRequest) returns (OrderResponse);
rpc CreateOrder (CreateOrderRequest) returns (OrderResponse);
// Server streaming — one request, stream of responses
rpc ListOrders (ListOrdersRequest) returns (stream OrderResponse);
// Client streaming — stream of requests, one response
rpc BulkCreateOrders (stream CreateOrderRequest) returns (BulkCreateResponse);
// Bidirectional streaming
rpc TrackOrders (stream TrackRequest) returns (stream TrackResponse);
}
message GetOrderRequest {
int32 order_id = 1;
}
message CreateOrderRequest {
int32 customer_id = 1;
repeated OrderItemMessage items = 2;
string delivery_address = 3;
}
message OrderItemMessage {
int32 product_id = 1;
int32 quantity = 2;
double unit_price = 3;
}
message OrderResponse {
int32 id = 1;
int32 customer_id = 2;
double total = 3;
string status = 4;
string created_at = 5;
}
message ListOrdersRequest {
int32 customer_id = 1;
int32 page_size = 2;
}
message BulkCreateResponse {
int32 created_count = 1;
repeated int32 order_ids = 2;
}
message TrackRequest {
int32 order_id = 1;
}
message TrackResponse {
int32 order_id = 1;
string status = 2;
string updated_at = 3;
}Step 2: Server Setup
<!-- OrderService.csproj -->
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.x.x" />
</ItemGroup>
<ItemGroup>
<!-- Tell the build system to generate C# from the .proto file -->
<Protobuf Include="Protos\orders.proto" GrpcServices="Server" />
</ItemGroup>// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
options.MaxReceiveMessageSize = 4 * 1024 * 1024; // 4MB
options.MaxSendMessageSize = 4 * 1024 * 1024;
});
builder.Services.AddGrpcReflection(); // for grpcurl and Postman
var app = builder.Build();
app.MapGrpcService<OrderGrpcService>();
if (app.Environment.IsDevelopment())
app.MapGrpcReflectionService();
app.Run();Step 3: Implement the Service
using Grpc.Core;
using OrderService; // generated namespace from .proto
public class OrderGrpcService(IOrderRepository repo, ILogger<OrderGrpcService> logger)
: OrderService.OrderServiceBase
{
// ── Unary RPC ────────────────────────────────────────────────────────────
public override async Task<OrderResponse> GetOrder(
GetOrderRequest request,
ServerCallContext context)
{
var order = await repo.GetByIdAsync(request.OrderId, context.CancellationToken);
if (order is null)
throw new RpcException(new Status(StatusCode.NotFound,
$"Order {request.OrderId} not found"));
return MapToResponse(order);
}
public override async Task<OrderResponse> CreateOrder(
CreateOrderRequest request,
ServerCallContext context)
{
var order = await repo.CreateAsync(new CreateOrderDto
{
CustomerId = request.CustomerId,
Items = request.Items.Select(i => new OrderItemDto(
i.ProductId, i.Quantity, (decimal)i.UnitPrice)).ToList(),
DeliveryAddress = request.DeliveryAddress,
}, context.CancellationToken);
return MapToResponse(order);
}
// ── Server Streaming ─────────────────────────────────────────────────────
public override async Task ListOrders(
ListOrdersRequest request,
IServerStreamWriter<OrderResponse> responseStream,
ServerCallContext context)
{
await foreach (var order in repo.GetByCustomerAsync(
request.CustomerId, request.PageSize, context.CancellationToken))
{
if (context.CancellationToken.IsCancellationRequested) break;
await responseStream.WriteAsync(MapToResponse(order));
}
}
// ── Client Streaming ─────────────────────────────────────────────────────
public override async Task<BulkCreateResponse> BulkCreateOrders(
IAsyncStreamReader<CreateOrderRequest> requestStream,
ServerCallContext context)
{
var orderIds = new List<int>();
await foreach (var request in requestStream.ReadAllAsync(context.CancellationToken))
{
var order = await repo.CreateAsync(new CreateOrderDto
{
CustomerId = request.CustomerId,
Items = request.Items.Select(i =>
new OrderItemDto(i.ProductId, i.Quantity, (decimal)i.UnitPrice)).ToList(),
DeliveryAddress = request.DeliveryAddress,
}, context.CancellationToken);
orderIds.Add(order.Id);
}
return new BulkCreateResponse
{
CreatedCount = orderIds.Count,
OrderIds = { orderIds },
};
}
// ── Bidirectional Streaming ───────────────────────────────────────────────
public override async Task TrackOrders(
IAsyncStreamReader<TrackRequest> requestStream,
IServerStreamWriter<TrackResponse> responseStream,
ServerCallContext context)
{
await foreach (var request in requestStream.ReadAllAsync(context.CancellationToken))
{
var status = await repo.GetStatusAsync(request.OrderId, context.CancellationToken);
await responseStream.WriteAsync(new TrackResponse
{
OrderId = request.OrderId,
Status = status,
UpdatedAt = DateTime.UtcNow.ToString("O"),
});
}
}
private static OrderResponse MapToResponse(Order order) => new()
{
Id = order.Id,
CustomerId = order.CustomerId,
Total = (double)order.Total,
Status = order.Status,
CreatedAt = order.CreatedAt.ToString("O"),
};
}Step 4: gRPC Client
<!-- Client project -->
<ItemGroup>
<PackageReference Include="Grpc.Net.Client" Version="2.x.x" />
<PackageReference Include="Google.Protobuf" Version="3.x.x" />
<PackageReference Include="Grpc.Tools" Version="2.x.x" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="Protos\orders.proto" GrpcServices="Client" />
</ItemGroup>// Register typed gRPC client in DI
builder.Services.AddGrpcClient<OrderService.OrderServiceClient>(options =>
{
options.Address = new Uri("https://order-service:5001");
})
.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler
{
// In production use proper TLS certs; in dev allow self-signed
ServerCertificateCustomValidationCallback =
HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
});
// Use the client
public class OrderApiController(OrderService.OrderServiceClient grpcClient) : ControllerBase
{
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id)
{
try
{
var response = await grpcClient.GetOrderAsync(
new GetOrderRequest { OrderId = id });
return Ok(response);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.NotFound)
{
return NotFound(ex.Status.Detail);
}
}
[HttpGet("stream/{customerId}")]
public async IAsyncEnumerable<OrderResponse> StreamOrders(
int customerId,
[EnumeratorCancellation] CancellationToken ct)
{
using var call = grpcClient.ListOrders(
new ListOrdersRequest { CustomerId = customerId, PageSize = 50 });
await foreach (var order in call.ResponseStream.ReadAllAsync(ct))
yield return order;
}
}Error Handling and Status Codes
// gRPC status codes map to HTTP status codes in grpc-gateway
// StatusCode.NotFound → 404
// StatusCode.InvalidArgument → 400
// StatusCode.Unauthenticated → 401
// StatusCode.PermissionDenied → 403
// StatusCode.Internal → 500
// StatusCode.Unavailable → 503
// Throw on server:
throw new RpcException(new Status(StatusCode.InvalidArgument,
"CustomerId must be positive"));
// Catch on client:
try
{
var result = await client.CreateOrderAsync(request);
}
catch (RpcException ex)
{
logger.LogError("gRPC error {Code}: {Detail}", ex.StatusCode, ex.Status.Detail);
// ex.Trailers — contains additional metadata from server
}Deadlines and Cancellation
// Always set a deadline on gRPC calls — prevents hanging indefinitely
var deadline = DateTime.UtcNow.AddSeconds(5);
var response = await client.GetOrderAsync(
new GetOrderRequest { OrderId = 42 },
deadline: deadline);
// Or use CancellationToken
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var response = await client.GetOrderAsync(
new GetOrderRequest { OrderId = 42 },
cancellationToken: cts.Token);Authentication with JWT
// Client: attach JWT Bearer token to every call
builder.Services.AddGrpcClient<OrderService.OrderServiceClient>(o =>
{
o.Address = new Uri("https://order-service:5001");
})
.AddCallCredentials(async (context, metadata, sp) =>
{
var tokenService = sp.GetRequiredService<ITokenService>();
var token = await tokenService.GetTokenAsync();
metadata.Add("Authorization", $"Bearer {token}");
});
// Server: validate JWT with standard ASP.NET Core auth
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => { /* standard JWT config */ });
builder.Services.AddAuthorization();
app.UseAuthentication();
app.UseAuthorization();
// Protect a gRPC method:
[Authorize]
public override async Task<OrderResponse> CreateOrder(
CreateOrderRequest request, ServerCallContext context)
{
var userId = context.GetHttpContext().User.FindFirstValue(ClaimTypes.NameIdentifier);
// ...
}Testing gRPC Services
// Test using WebApplicationFactory — same as REST
public class OrderGrpcServiceTests : IClassFixture<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public OrderGrpcServiceTests(WebApplicationFactory<Program> factory)
=> _factory = factory;
[Fact]
public async Task GetOrder_Existing_ReturnsOrder()
{
// Create gRPC channel pointing at the test server
var channel = GrpcChannel.ForAddress(
_factory.Server.BaseAddress,
new GrpcChannelOptions { HttpClient = _factory.CreateClient() });
var client = new OrderService.OrderServiceClient(channel);
var response = await client.GetOrderAsync(new GetOrderRequest { OrderId = 1 });
Assert.Equal(1, response.Id);
}
[Fact]
public async Task GetOrder_NotFound_ThrowsRpcException()
{
var channel = GrpcChannel.ForAddress(_factory.Server.BaseAddress,
new GrpcChannelOptions { HttpClient = _factory.CreateClient() });
var client = new OrderService.OrderServiceClient(channel);
var ex = await Assert.ThrowsAsync<RpcException>(
() => client.GetOrderAsync(new GetOrderRequest { OrderId = 9999 }).ResponseAsync);
Assert.Equal(StatusCode.NotFound, ex.StatusCode);
}
}Interview Answer
"gRPC uses HTTP/2 and Protocol Buffers — a binary serialisation format — giving 5–10x smaller payloads and strongly typed contracts compared to REST+JSON. The
.protofile is the single source of truth; Grpc.Tools generates server base classes and client stubs automatically. Four RPC types: unary (one request → one response), server streaming (one request → stream), client streaming (stream → one response), bidirectional (stream ↔ stream). In ASP.NET Core: installGrpc.AspNetCore, add the proto file asGrpcServices=Server, inherit from the generated base class, register withMapGrpcService. On the client:Grpc.Net.ClientwithAddGrpcClientin DI, same proto withGrpcServices=Client. Always set deadlines on client calls — gRPC propagates cancellation through the call chain. Error handling usesRpcExceptionwithStatusCode(NotFound, InvalidArgument, Internal). Testing:GrpcChannel.ForAddresswithWebApplicationFactory.CreateClient()— same pattern as REST integration tests."