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 Type | Requests | Responses |
|---|---|---|
| Unary | 1 | 1 |
| Client Streaming | Many | 1 |
| Server Streaming | 1 | Many |
| Bidirectional Streaming | Many | Many |
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.
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.
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 / Method | Type | What it gives you |
|---|---|---|
context.Peer | string | The client's address, e.g. ipv4:127.0.0.1:54321 |
context.Host | string | The host the client connected to |
context.Method | string | Full method path, e.g. /greet.Greetings/SayHello |
context.Deadline | DateTime | When the call will time out (if client set one) |
context.CancellationToken | CancellationToken | Fires when the client cancels or deadline is reached |
context.RequestHeaders | Metadata | Headers sent by the client (e.g. authentication tokens) |
context.ResponseTrailers | Metadata | Trailing 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.
| Version | Method Name | Returns | Blocks the thread? |
|---|---|---|---|
| Blocking | client.SayHello(request) | HelloReply | Yes — thread waits |
| Async | client.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.
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:
| Property | Type | What it contains |
|---|---|---|
.ResponseAsync | Task<HelloReply> | The actual response message from the server |
.ResponseHeadersAsync | Task<Metadata> | HTTP/2 headers sent by the server before the response body |
.GetStatus() | Status | The gRPC status code (OK, NOT_FOUND, etc.) — available after response |
.GetTrailers() | Metadata | Trailing metadata from the server — available after response |
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.
| Option | Type | Default | Description |
|---|---|---|---|
Credentials | ChannelCredentials | Insecure | TLS / authentication credentials for the connection |
HttpClient | HttpClient? | null (creates one internally) | Custom HttpClient to use for all requests |
DisposeHttpClient | bool | false | Set to true if the channel should own and dispose the HttpClient |
HttpHandler | HttpMessageHandler? | null | Custom HTTP handler (e.g. for mocking in tests) |
LoggerFactory | ILoggerFactory? | null | Enables gRPC client-side logging |
MaxReceiveMessageSize | int? | 4 MB | Maximum size of an incoming message in bytes |
MaxSendMessageSize | int? | unlimited | Maximum size of an outgoing message in bytes |
CompressionProviders | IList? | null | Compression 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.
| Scenario | URL Pattern | Example |
|---|---|---|
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:
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:
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:
Summary
- A unary call sends one request and receives one response — the simplest call type.
- gRPC generates a blocking and an async version of each RPC method; always prefer async.
- The async version returns
AsyncUnaryCall<T>which exposes.ResponseAsyncand.ResponseHeadersAsync. ServerCallContextgives the server access to client metadata, peer address, deadline, and cancellation.- Use
GrpcChannelOptionsto configure message size limits, credentials, and logging. - The URL route is
/{package}.{Service}/{Method}— mismatched packages cause anUnknownerror. - Use
global::to resolve namespace conflicts with generated proto types.