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

Making Unary Calls in gRPC with .NET

1. What is a Unary Call?

A unary call is the simplest gRPC call type. It works exactly like a normal function call or an HTTP request:

Think of a unary call like an ATM machine: you insert your card (request), the machine does its job, then hands you cash (response). One request, one response, done.
Call TypeRequestsResponses
Unary11
Client StreamingMany1
Server Streaming1Many
Bidirectional StreamingManyMany

2. The Proto Contract

The proto file in GrpcDependencies/Protos/greet.proto is the shared contract between the server and the client. The SayHello method is a unary call — no stream keyword on either side.

syntax = "proto3";

option csharp_namespace = "BasicGrpcService";

package greet;

service Greetings {
// Unary call: one request, one response — no "stream" keyword
rpc SayHello (HelloRequest) returns (HelloReply);

// Other call types are defined below (covered in separate articles)
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;
}

The key rule is simple: if neither request nor response has the stream keyword, the call is unary.

3. Server Implementation

The server in GrpcService1/Services/GreeterService.cs inherits from the generated Greetings.GreetingsBase class and overrides SayHello.

using BasicGrpcService;
using Grpc.Core;

namespace GrpcService1.Services
{
public class GreeterService(ILogger<GreeterService> logger) : Greetings.GreetingsBase
{
// Unary: one request -> one response
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
logger.LogInformation(
"SayHello called from Peer={Peer}, Host={Host}",
context.Peer,
context.Host);

return Task.FromResult(new HelloReply
{
Message = "Hello " + request.Name
});
}
}
}

Notice the method signature: it receives a HelloRequest object and a ServerCallContext, and returns a Task<HelloReply>.

4. Understanding ServerCallContext

ServerCallContext is automatically passed to every RPC method by the gRPC runtime. It carries metadata about the current call. You do not create it — gRPC creates it for you.

Property / MethodTypeWhat it gives you
context.PeerstringThe client's address, e.g. ipv4:127.0.0.1:54321
context.HoststringThe host the client connected to
context.MethodstringFull method path, e.g. /greet.Greetings/SayHello
context.DeadlineDateTimeWhen the call will time out (if client set one)
context.CancellationTokenCancellationTokenFires when the client cancels or deadline is reached
context.RequestHeadersMetadataHeaders sent by the client (e.g. authentication tokens)
context.ResponseTrailersMetadataTrailing metadata you send back to the client

5. Blocking Call vs Async Call

When calling an RPC from the client, gRPC generates two versions of each method: a blocking (synchronous) version and an async version.

Think of blocking vs async like ordering food at a restaurant. Blocking means you stand at the counter and wait until your food is ready before doing anything else. Async means you take a buzzer, sit down, and continue chatting until the buzzer goes off.
VersionMethod NameReturnsBlocks the thread?
Blockingclient.SayHello(request)HelloReplyYes — thread waits
Asyncclient.SayHelloAsync(request)AsyncUnaryCall<HelloReply>No — thread is free to do other work

Always prefer the async version in real applications. The blocking version exists mostly for compatibility with older synchronous code.

// BLOCKING — the thread is frozen until the server responds
var reply = client.SayHello(new HelloRequest { Name = "Ahmet" });
Console.WriteLine(reply.Message);

// ASYNC — the thread is free while waiting; much better for performance
var reply = await client.SayHelloAsync(new HelloRequest { Name = "Ahmet" });
Console.WriteLine(reply.Message);

6. The AsyncUnaryCall Object

SayHelloAsync returns an AsyncUnaryCall<HelloReply> object — not the response directly. This object wraps the ongoing call and exposes two awaitable properties:

PropertyTypeWhat it contains
.ResponseAsyncTask<HelloReply>The actual response message from the server
.ResponseHeadersAsyncTask<Metadata>HTTP/2 headers sent by the server before the response body
.GetStatus()StatusThe gRPC status code (OK, NOT_FOUND, etc.) — available after response
.GetTrailers()MetadataTrailing metadata from the server — available after response
// Get the call object first — the call starts immediately
var call = client.SayHelloAsync(new HelloRequest { Name = "Ahmet" });

// Optionally read the headers before the response arrives
var headers = await call.ResponseHeadersAsync;
foreach (var header in headers)
{
Console.WriteLine($"Header: {header.Key} = {header.Value}");
}

// Wait for the actual response
var reply = await call.ResponseAsync;
Console.WriteLine("Message: " + reply.Message);

// Read status and trailers after the response
var status = call.GetStatus();
Console.WriteLine("Status: " + status.StatusCode);

In most day-to-day code you just write await client.SayHelloAsync(request) which implicitly awaits ResponseAsync. The explicit form above is useful when you need headers or trailers.

7. GrpcChannelOptions Reference

When you create a channel with GrpcChannel.ForAddress(), you can pass a GrpcChannelOptions object to configure how the channel behaves.

using var channel = GrpcChannel.ForAddress("http://localhost:5287", new GrpcChannelOptions
{
MaxReceiveMessageSize = 5 * 1024 * 1024, // 5 MB
MaxSendMessageSize = 5 * 1024 * 1024, // 5 MB
});
OptionTypeDefaultDescription
CredentialsChannelCredentialsInsecureTLS / authentication credentials for the connection
HttpClientHttpClient?null (creates one internally)Custom HttpClient to use for all requests
DisposeHttpClientboolfalseSet to true if the channel should own and dispose the HttpClient
HttpHandlerHttpMessageHandler?nullCustom HTTP handler (e.g. for mocking in tests)
LoggerFactoryILoggerFactory?nullEnables gRPC client-side logging
MaxReceiveMessageSizeint?4 MBMaximum size of an incoming message in bytes
MaxSendMessageSizeint?unlimitedMaximum size of an outgoing message in bytes
CompressionProvidersIList?nullCompression algorithms to offer (e.g. gzip)

8. URL Routing in gRPC

gRPC uses HTTP/2 under the hood. Every RPC method maps to a URL path. The path format depends on whether your proto file has a package declaration.

ScenarioURL PatternExample
With package/{package}.{ServiceName}/{MethodName}/greet.Greetings/SayHello
Without package/{ServiceName}/{MethodName}/Greetings/SayHello

Our greet.proto has package greet;, so the full URL is:

POST http://localhost:5287/greet.Greetings/SayHello

Important: If the client and server use different package names, or if one has a package and the other does not, the server will return an Unknown gRPC error because the route does not match. Always make sure the proto files on both sides are identical.

9. Resolving Namespace Conflicts

The proto file uses option csharp_namespace = "BasicGrpcService"; to set the C# namespace of the generated classes. If your project already has a class named HelloRequest in another namespace, you may get a compiler error.

In that case, use the global:: prefix to refer to the generated type unambiguously:

// Without global:: — may be ambiguous if both namespaces are imported
var request = new HelloRequest { Name = "Ahmet" };

// With global:: — always resolves to the correct generated type
var request = new global::BasicGrpcService.HelloRequest { Name = "Ahmet" };

This tells the C# compiler to start looking from the root of the namespace tree, so there is no ambiguity.

10. Full Client Example

Here is the complete BasicGrpcClient/Program.cs demonstrating a unary call with all the concepts above applied:

using BasicGrpcService;
using Grpc.Net.Client;

// Create and reuse a single channel for the lifetime of the application.
// This is efficient — channels are expensive to create.
using var channel = GrpcChannel.ForAddress("http://localhost:5287", new GrpcChannelOptions
{
MaxReceiveMessageSize = 5 * 1024 * 1024, // 5 MB limit on incoming messages
MaxSendMessageSize = 5 * 1024 * 1024, // 5 MB limit on outgoing messages
});

// Generate the client stub from the channel.
var client = new Greetings.GreetingsClient(channel);

// --- ASYNC UNARY CALL (recommended approach) ---

// SayHelloAsync returns an AsyncUnaryCall<HelloReply> object.
// Awaiting it directly gives us the HelloReply response.
var reply = await client.SayHelloAsync(new HelloRequest { Name = "Ahmet" });
Console.WriteLine("Response: " + reply.Message);

// --- USING THE CALL OBJECT TO READ HEADERS FIRST ---

var call = client.SayHelloAsync(new HelloRequest { Name = "Mehmet" });

// ResponseHeadersAsync completes as soon as the server sends the response headers,
// which happens before the response body. Useful for auth tokens or tracing IDs.
var headers = await call.ResponseHeadersAsync;
Console.WriteLine($"Received {headers.Count} response headers");

// Now wait for the actual response message.
var detailedReply = await call.ResponseAsync;
Console.WriteLine("Detailed Response: " + detailedReply.Message);

// Status and trailers are available after the response is fully received.
var status = call.GetStatus();
Console.WriteLine("gRPC Status: " + status.StatusCode);

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

Summary

  1. A unary call sends one request and receives one response — the simplest call type.
  2. gRPC generates a blocking and an async version of each RPC method; always prefer async.
  3. The async version returns AsyncUnaryCall<T> which exposes .ResponseAsync and .ResponseHeadersAsync.
  4. ServerCallContext gives the server access to client metadata, peer address, deadline, and cancellation.
  5. Use GrpcChannelOptions to configure message size limits, credentials, and logging.
  6. The URL route is /{package}.{Service}/{Method} — mismatched packages cause an Unknown error.
  7. Use global:: to resolve namespace conflicts with generated proto types.


Share this lesson: