Client Streaming in gRPC with .NET
1. What is Client Streaming?
In a client streaming call, the client sends multiple messages to the server through an open stream. When the client is done, it closes the stream and the server responds with a single response.
Think of client streaming like a customer depositing many coins into a coin-counting machine. The customer drops coins one by one (multiple requests). When done, the machine prints one receipt with the total (single response).
| Call Type | Client Sends | Server Responds |
|---|---|---|
| Unary | 1 message | 1 message |
| Client Streaming | Many messages | 1 message (after stream ends) |
| Server Streaming | 1 message | Many messages |
| Bidirectional Streaming | Many messages | Many messages |
Common use cases:
- Uploading a large file in chunks
- Sending a batch of sensor readings
- Sending multiple items to be aggregated (sum, count, etc.)
2. Adding stream to the Proto Request
To declare a client streaming RPC, add the stream keyword before the request type only. The response has no stream keyword.
The stream keyword on the request side tells gRPC: "the client will send multiple HelloRequest messages before expecting one HelloReply back."
3. Server Implementation
On the server side (GrpcService1/Services/GreeterService.cs), the method signature changes compared to unary calls. Instead of receiving a single HelloRequest, the server receives an IAsyncStreamReader<HelloRequest>.
Key points about the server method:
- The method is
asyncbecause reading a stream is an asynchronous operation. - The method does not return until after the entire client stream has been read.
- The server can process each message as it arrives — it does not need to wait for all of them.
4. Reading the Stream on the Server
The server receives an IAsyncStreamReader<T>. There are two ways to read from it:
Option A: ReadAllAsync() — Recommended
ReadAllAsync() returns an IAsyncEnumerable<T> that you iterate with await foreach. This is the cleanest and most modern approach.
Option B: MoveNext() + Current — Manual approach
MoveNext() advances the reader to the next message and returns true if a message was available. Current holds the message. This is more verbose but gives you more control.
| Approach | When to use |
|---|---|
ReadAllAsync() + await foreach | Most cases — clean and readable |
MoveNext() + Current | When you need a CancellationToken or older C# versions |
5. Client Implementation
On the client side (BasicGrpcClient/Program.cs), calling a client streaming method opens the stream immediately but does not send anything yet. You then use WriteAsync() to send each message, and CompleteAsync() to signal that you are done sending.
6. The AsyncClientStreamingCall Object
client.SendMultipleHellos() returns an AsyncClientStreamingCall<HelloRequest, HelloReply>. This object represents the entire ongoing communication session.
| Property / Method | Type | What it does |
|---|---|---|
.RequestStream | IClientStreamWriter<HelloRequest> | Use this to send messages to the server |
.RequestStream.WriteAsync(msg) | Task | Sends one message to the server |
.RequestStream.CompleteAsync() | Task | Tells the server you are done sending (closes the write side) |
await call | HelloReply | Waits for and returns the server's single response |
.ResponseHeadersAsync | Task<Metadata> | The response headers sent by the server |
Important: You must call CompleteAsync() before awaiting the response. If you forget, the server will keep waiting for more messages and the call will hang forever.
7. Step-by-Step Streaming Flow
Here is exactly what happens during a client streaming call:
- Client calls
SendMultipleHellos()— The HTTP/2 connection is opened. The stream is now active. No messages have been sent yet. - Client calls
RequestStream.WriteAsync(msg1)— The firstHelloRequestis serialized and sent over the wire. The server'srequestStream.MoveNext()(orReadAllAsync()) receives it. - Client calls
WriteAsync(msg2),WriteAsync(msg3)— More messages flow to the server. The server processes them as they arrive. - Client calls
CompleteAsync()— A special "half-close" signal is sent. The server'sReadAllAsync()loop ends. - Server returns the response — The server finishes processing and sends back one
HelloReply. - Client
await callcompletes — The client receives the reply.
8. Full Working Example
Below is the complete code for both projects to demonstrate client streaming end-to-end.
GrpcDependencies/Protos/greet.proto
GrpcService1/Services/GreeterService.cs
BasicGrpcClient/Program.cs
Expected output:
Summary
- Client streaming uses the
streamkeyword before the request type in the proto. - The server receives
IAsyncStreamReader<T>instead of the message directly. - Use
ReadAllAsync()on the server for clean iteration; useMoveNext()for manual control. - The client receives
AsyncClientStreamingCall<TRequest, TResponse>from the RPC method. - Use
RequestStream.WriteAsync()to send each message. - Always call
RequestStream.CompleteAsync()when done — this is how the server knows to send its response. - Then
await callto get the server's single response.