Skip to content
S
distributed systems

Building a gRPC Client in .NET

Updated · originally #grpc#dotnet#csharp4 min read

Introduction

In this article, we will build a .NET gRPC client from a shared Protocol Buffers contract, reuse a channel, set an RPC deadline, and call the ASP.NET Core gRPC server from a console application.

The generated client keeps the call site small, but a production client still needs deliberate transport security, deadlines, status handling, retries, and observability.

Plan

The plan for this article is as follows.

  1. Scaffold a .NET console project.
  2. Implementing the gRPC client.
  3. Communicating with the server.

In a nutshell, we will be generating the client for the server we built in our previous post.

building-grpc-client-dotnet-1.png

💡  As always, all the code samples documentation can be found at: https://github.com/sahansera/dotnet-grpc

Prerequisites

  • .NET 10 SDK
  • Visual Studio Code or IDE of your choice

The Grpc.Tools package runs code generation through MSBuild, so this walkthrough does not require a separate protoc installation.

Project Structure

We can use .NET’s tooling to generate a sample gRPC project. Run the following command at the root of your workspace. Remember how we used dotnet new grpc command to scaffold the server project? For this one though, it can simply be a console app.

Shell
dotnet new console -o BookshopClient

Your project structure should look like this.

building-grpc-client-dotnet-2.png

You must be wondering if this is a console app how does it know how to generate the client stubs? Well, it doesn’t. You have to add the following packages to the project first.

Shell
dotnet add BookshopClient/BookshopClient.csproj package Grpc.Net.Client
dotnet add BookshopClient/BookshopClient.csproj package Google.Protobuf
dotnet add BookshopClient/BookshopClient.csproj package Grpc.Tools

Once everything’s installed, we can proceed with the rest of the steps.

Generating the client stubs

We will be using the same Protobuf files that we generated in our previous step. If you haven’t seen that already head over to my previous post.

Open up the BookshopClient.csproj file you need to add the following lines:

XML
...
<ItemGroup>
  <Protobuf Include="../proto/bookshop.proto" GrpcServices="Client" />
</ItemGroup>
...

As you can see we will be reusing our Bookshop.proto file. in this example too. One thing to note here is that we have updated the GrpcServices attribute to be Client.

Implementing the gRPC client

Let’s update Program.cs to create the channel and call the server with a five-second deadline.

C#
using Grpc.Core;
using Grpc.Net.Client;
using Bookshop;

// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("http://localhost:5000");
var client = new Inventory.InventoryClient(channel);

try
{
  var reply = await client.GetBookListAsync(
      new GetBookListRequest(),
      deadline: DateTime.UtcNow.AddSeconds(5));

  Console.WriteLine("Books: " + reply.Books);
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
  Console.Error.WriteLine("The book list request exceeded its five-second deadline.");
  Environment.ExitCode = 1;
}

This is based on the example given on the Microsoft docs site btw. What I really like about the above code is how easy it is to read. So here’s what happens.

  1. GrpcChannel.ForAddress creates a channel for the server URI. Channels are designed to be long-lived and shared; creating one per call repeats connection and HTTP/2 setup work.
  2. Inventory.InventoryClient is generated from bookshop.proto. Multiple lightweight generated clients can reuse the same channel.
  3. GetBookListAsync makes the unary call with a five-second deadline. Without a deadline, a call can continue waiting while a dependency is unavailable.
  4. A deadline failure arrives as an RpcException with StatusCode.DeadlineExceeded, which lets the caller handle it separately from other gRPC statuses.

Now that we know how the requests work, let’s see this in action.

Communicating with the server

Let’s spin up the server that we built in my previous post first. This will be up and running at port 5000.

Shell
dotnet run --project BookshopServer/BookshopServer.csproj

building-grpc-client-dotnet-4.png

For the client-side, we invoke a similar command.

Shell
dotnet run --project BookshopClient/BookshopClient.csproj

And in the terminal, we will get the following outputs.

Nice! As you can see, it’s not hard to get the generated client working 🎉 The companion repository uses plaintext HTTP/2 only for local development. Use TLS and authentication across an untrusted network, and only retry status codes that are safe for the semantics of the RPC.

Conclusion

In this article, we reused the server’s Protocol Buffers contract, created one long-lived channel, and bounded the unary call with a deadline. Those choices keep the client typed and efficient without allowing a failed dependency to hold the call indefinitely.

I hope this article series cleared up a lot of confusion that you had about gRPC. Please feel free to share your questions, thoughts, or feedback in the comments section below. Until next time 👋

The obvious next question is where to run it. gRPC needs HTTP/2 end to end, which not every managed platform gives you by default, and deploying a .NET gRPC server on Azure App Service goes through what that takes.

References

Stay in the loop

Practical engineering notes, without the inbox noise.

Notes on distributed systems, resilient software, and engineering in the real world - usually once or twice a month.

Unsubscribe anytime. See what you get, or prefer a feed? Subscribe via RSS.