Unary & Server Streaming RPCs
Implement unary calls and server-side streaming to push data from server to client in real time.
Unary & Server Streaming RPCs is a free C# Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the C# Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
gRPC RPC Types
gRPC supports four RPC patterns: Unary (request/response), Server Streaming (client sends one, server sends many), Client Streaming, and Bidirectional Streaming. This lesson covers the first two.
Unary RPC: The Basic Pattern
Unary is the simplest pattern — one request, one response. It looks like a regular function call but travels over HTTP/2 with Protobuf encoding.
// .proto definition
service OrderService {
rpc GetOrder (GetOrderRequest) returns (OrderResponse);
}
message GetOrderRequest { int32 id = 1; }
message OrderResponse { int32 id = 1; string status = 2; double total = 3; }Implementing a Unary RPC
Override the generated base method. The ServerCallContext provides metadata, cancellation token, deadline, and peer info.
public class OrderService : OrderService.OrderServiceBase
{
private readonly IOrderRepository _repo;
public OrderService(IOrderRepository repo) => _repo = repo;
public override async Task<OrderResponse> GetOrder(
GetOrderRequest request,
ServerCallContext context)
{
var order = await _repo.GetByIdAsync(request.Id, context.CancellationToken);
if (order is null)
throw new RpcException(new Status(StatusCode.NotFound, "Order not found"));
return new OrderResponse
{
Id = order.Id,
Status = order.Status.ToString(),
Total = (double)order.Total
};
}
}Calling a Unary RPC from a Client
The generated client provides both synchronous-looking and truly async methods. Always use the Async variant in production.
var client = new OrderService.OrderServiceClient(channel);
// Async call with cancellation
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var reply = await client.GetOrderAsync(
new GetOrderRequest { Id = 42 },
cancellationToken: cts.Token);
Console.WriteLine($"Order {reply.Id}: {reply.Status} - ${reply.Total}");Server Streaming: Proto Definition
Server streaming adds the stream keyword before the response type. The server sends multiple messages on the same connection before closing the stream.
service StockService {
// Client sends one symbol, server streams price updates
rpc WatchStock (WatchRequest) returns (stream StockUpdate);
}
message WatchRequest { string symbol = 1; }
message StockUpdate { string symbol = 1; double price = 2; int64 timestamp = 3; }Implementing Server Streaming
Use IServerStreamWriter<T> to send messages. The method returns a Task — write until done, then return.
public override async Task WatchStock(
WatchRequest request,
IServerStreamWriter<StockUpdate> responseStream,
ServerCallContext context)
{
while (!context.CancellationToken.IsCancellationRequested)
{
var price = await _market.GetPriceAsync(request.Symbol);
await responseStream.WriteAsync(new StockUpdate
{
Symbol = request.Symbol,
Price = price,
Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
});
await Task.Delay(1000, context.CancellationToken);
}
}Consuming a Server Stream from the Client
Use await foreach on ResponseStream.ReadAllAsync() to process each message as it arrives.
using var call = client.WatchStock(
new WatchRequest { Symbol = "MSFT" });
await foreach (var update in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine($"{update.Symbol}: ${update.Price:F2}");
}
// Continues until server closes the stream or client cancelsCancelling a Server Stream
Pass a CancellationToken when opening the call to stop the stream client-side. The server receives the cancellation via its own token.
var cts = new CancellationTokenSource();
// Cancel after 30 seconds
cts.CancelAfter(TimeSpan.FromSeconds(30));
using var call = client.WatchStock(
new WatchRequest { Symbol = "AAPL" },
cancellationToken: cts.Token);
try
{
await foreach (var update in
call.ResponseStream.ReadAllAsync(cts.Token))
{
Console.WriteLine($"Price: {update.Price}");
}
}
catch (OperationCanceledException)
{
Console.WriteLine("Stream cancelled.");
}Metadata: Request Headers and Trailers
gRPC supports metadata (key-value pairs) sent as headers before the response and trailers after the response. Use them for auth tokens, tracing IDs, or pagination cursors.
// Server: send initial metadata
await context.WriteResponseHeadersAsync(new Metadata
{
{ "x-correlation-id", Guid.NewGuid().ToString() }
});
// Server: set trailing metadata
context.ResponseTrailers.Add("x-total-count", "1500");
// Client: read headers
var headers = await call.ResponseHeadersAsync;
var trailerId = headers.GetValue("x-correlation-id");gRPC-Web for Browser Clients
Browsers can't use raw gRPC (HTTP/2 trailers not supported). Use Grpc.AspNetCore.Web and the grpc-web JavaScript client or Blazor's Grpc.Net.Client.Web.
// dotnet add package Grpc.AspNetCore.Web
app.UseGrpcWeb();
app.MapGrpcService<GreeterService>().EnableGrpcWeb();
// Blazor client:
var handler = new GrpcWebHandler(GrpcWebMode.GrpcWeb,
new HttpClientHandler());
using var channel = GrpcChannel.ForAddress(
"https://localhost:7042",
new GrpcChannelOptions { HttpHandler = handler });Real-World: Log Streaming Service
A log aggregation service streams application log entries to connected monitoring clients in real time using server streaming.
public override async Task StreamLogs(
LogStreamRequest request,
IServerStreamWriter<LogEntry> responseStream,
ServerCallContext context)
{
await foreach (var log in _logChannel.Reader.ReadAllAsync(
context.CancellationToken))
{
if (log.Level >= request.MinLevel)
{
await responseStream.WriteAsync(new LogEntry
{
Level = log.Level.ToString(),
Message = log.Message,
Timestamp = Timestamp.FromDateTime(log.Timestamp)
});
}
}
}Quick Check
What keyword in a .proto file indicates a server streaming RPC?
Recap: Unary & Server Streaming RPCs
Key takeaways:
- Unary RPC: one request, one response — the most common pattern
- Server streaming: one request, multiple response messages over time
- Implement server streaming by writing to
IServerStreamWriter<T> - Consume server streams with
await foreachonReadAllAsync() - Use CancellationToken to stop streams cleanly from either side
- gRPC-Web allows browser clients to call gRPC services
Frequently asked questions
Is the “Unary & Server Streaming RPCs” lesson free?
Yes — the full text of “Unary & Server Streaming RPCs” is free to read here on the web, and the C# Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the C# Academy course, upgrade to CoddyKit PRO.
What will I learn in “Unary & Server Streaming RPCs”?
Implement unary calls and server-side streaming to push data from server to client in real time. You practise C# Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start C# Academy?
No prior experience is required. C# Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Unary & Server Streaming RPCs” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this C# Academy lesson?
Yes. Every C# Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- gRPC & Protobuf Fundamentals
- Unary & Server Streaming RPCs
- Client & Bidirectional Streaming
- Deadlines, Cancellation & Interceptors