gRPC With. Net Call Types Supported by gRPC Created: 26 Mar 2026 Updated: 26 Mar 2026

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 TypeClient SendsServer Responds
Unary1 message1 message
Client StreamingMany messages1 message (after stream ends)
Server Streaming1 messageMany messages
Bidirectional StreamingMany messagesMany messages

Common use cases:

  1. Uploading a large file in chunks
  2. Sending a batch of sensor readings
  3. 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.

syntax = "proto3";

option csharp_namespace = "BasicGrpcService";

package greet;

service Greetings {
// Unary — no stream keyword
rpc SayHello (HelloRequest) returns (HelloReply);

// Client streaming — "stream" is on the REQUEST side only
rpc SendMultipleHellos (stream HelloRequest) returns (HelloReply);

// (other call types omitted for brevity)
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

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>.

using BasicGrpcService;
using Grpc.Core;

namespace GrpcService1.Services
{
public class GreeterService(ILogger<GreeterService> logger) : Greetings.GreetingsBase
{
// Client streaming: client sends many requests -> server responds once when stream ends
public override async Task<HelloReply> SendMultipleHellos(
IAsyncStreamReader<HelloRequest> requestStream,
ServerCallContext context)
{
var names = new List<string>();

// Read every message the client sends until the stream is closed
await foreach (var request in requestStream.ReadAllAsync())
{
logger.LogInformation("Received name: {Name}", request.Name);
names.Add(request.Name);
}

// After the stream ends, send a single response
return new HelloReply { Message = "Hello " + string.Join(", ", names) };
}
}
}

Key points about the server method:

  1. The method is async because reading a stream is an asynchronous operation.
  2. The method does not return until after the entire client stream has been read.
  3. 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.

await foreach (var request in requestStream.ReadAllAsync())
{
Console.WriteLine("Got: " + request.Name);
}
// When the loop ends, the stream is fully consumed

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.

while (await requestStream.MoveNext())
{
var request = requestStream.Current;
Console.WriteLine("Got: " + request.Name);
}
// When MoveNext returns false, the stream is done
ApproachWhen to use
ReadAllAsync() + await foreachMost cases — clean and readable
MoveNext() + CurrentWhen 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.

using BasicGrpcService;
using Grpc.Net.Client;

using var channel = GrpcChannel.ForAddress("http://localhost:5287");
var client = new Greetings.GreetingsClient(channel);

// Open the client streaming call — the connection is established here
using var call = client.SendMultipleHellos();

// Send messages one by one through the open stream
await call.RequestStream.WriteAsync(new HelloRequest { Name = "Ahmet" });
await call.RequestStream.WriteAsync(new HelloRequest { Name = "Ayse" });
await call.RequestStream.WriteAsync(new HelloRequest { Name = "Mehmet" });

// Signal to the server that we are done sending — this is mandatory!
await call.RequestStream.CompleteAsync();

// Now wait for the server's single response
var reply = await call;
Console.WriteLine("Response: " + reply.Message);
// Output: Response: Hello Ahmet, Ayse, Mehmet

6. The AsyncClientStreamingCall Object

client.SendMultipleHellos() returns an AsyncClientStreamingCall<HelloRequest, HelloReply>. This object represents the entire ongoing communication session.

Property / MethodTypeWhat it does
.RequestStreamIClientStreamWriter<HelloRequest>Use this to send messages to the server
.RequestStream.WriteAsync(msg)TaskSends one message to the server
.RequestStream.CompleteAsync()TaskTells the server you are done sending (closes the write side)
await callHelloReplyWaits for and returns the server's single response
.ResponseHeadersAsyncTask<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:

  1. Client calls SendMultipleHellos() — The HTTP/2 connection is opened. The stream is now active. No messages have been sent yet.
  2. Client calls RequestStream.WriteAsync(msg1) — The first HelloRequest is serialized and sent over the wire. The server's requestStream.MoveNext() (or ReadAllAsync()) receives it.
  3. Client calls WriteAsync(msg2), WriteAsync(msg3) — More messages flow to the server. The server processes them as they arrive.
  4. Client calls CompleteAsync() — A special "half-close" signal is sent. The server's ReadAllAsync() loop ends.
  5. Server returns the response — The server finishes processing and sends back one HelloReply.
  6. Client await call completes — 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

syntax = "proto3";

option csharp_namespace = "BasicGrpcService";

package greet;

service Greetings {
rpc SayHello (HelloRequest) returns (HelloReply);
rpc SendMultipleHellos (stream HelloRequest) returns (HelloReply);
rpc GetMultipleReplies (HelloRequest) returns (stream HelloReply);
rpc Chat (stream HelloRequest) returns (stream HelloReply);
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

GrpcService1/Services/GreeterService.cs

using BasicGrpcService;
using Grpc.Core;

namespace GrpcService1.Services
{
public class GreeterService(ILogger<GreeterService> logger) : Greetings.GreetingsBase
{
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply { Message = "Hello " + request.Name });
}

public override async Task<HelloReply> SendMultipleHellos(
IAsyncStreamReader<HelloRequest> requestStream,
ServerCallContext context)
{
var names = new List<string>();

await foreach (var request in requestStream.ReadAllAsync())
{
logger.LogInformation("Received name: {Name}", request.Name);
names.Add(request.Name);
}

return new HelloReply { Message = "Hello " + string.Join(", ", names) };
}
}
}

BasicGrpcClient/Program.cs

using BasicGrpcService;
using Grpc.Net.Client;

using var channel = GrpcChannel.ForAddress("http://localhost:5287");
var client = new Greetings.GreetingsClient(channel);

Console.WriteLine("=== Client Streaming Demo ===");

// Open the streaming call
using var call = client.SendMultipleHellos();

// Prepare the names to send
var names = new[] { "Ahmet", "Ayse", "Mehmet", "Fatma" };

foreach (var name in names)
{
Console.WriteLine("Sending: " + name);
await call.RequestStream.WriteAsync(new HelloRequest { Name = name });
await Task.Delay(300); // simulate delay between messages
}

// Close the stream — server will now send its response
await call.RequestStream.CompleteAsync();

// Wait for the server's single response
var reply = await call;
Console.WriteLine("Server responded: " + reply.Message);

Console.WriteLine("Press any key to exit...");
Console.ReadKey();

Expected output:

=== Client Streaming Demo ===
Sending: Ahmet
Sending: Ayse
Sending: Mehmet
Sending: Fatma
Server responded: Hello Ahmet, Ayse, Mehmet, Fatma

Summary

  1. Client streaming uses the stream keyword before the request type in the proto.
  2. The server receives IAsyncStreamReader<T> instead of the message directly.
  3. Use ReadAllAsync() on the server for clean iteration; use MoveNext() for manual control.
  4. The client receives AsyncClientStreamingCall<TRequest, TResponse> from the RPC method.
  5. Use RequestStream.WriteAsync() to send each message.
  6. Always call RequestStream.CompleteAsync() when done — this is how the server knows to send its response.
  7. Then await call to get the server's single response.


Share this lesson: