Playing with gRPC and .NET 6: Client Side

moises zapata
Published: September 7, 2022

In my previous article, I focused on how we can create a gRPC server using .NET. Now, I want to show you how we can create a gRPC client using .NET.

In the next image, I mark the items that we will focus on this tutorial.

Prerequisites

I’m using macOS, and the next commands are specific to this OS:

  • gRPC compiler: To install Protobuf compiler, you can execute this in a terminal:
brew install protobuf
  • .NET 6 SDK: Here you can find the links to download and install the .NET 6 SDK
  • Visual Studio Code or IDE of your choice
  • grpcurl: A command-line tool that provides interaction with gRPC services
brew install grpcurl
  • grpcui: builds on top of gRPCurl and adds an interactive web UI for gRPC, similar to tools like Postman and Swagger UI
brew install grpcui

Steps

In a general manner, we should follow the next steps:

  1. Create a project.
  2. Add the gRPC dependencies to the project.
  3. Add the proto file on your project.
  4. Register the created proto file and compile the project.
  5. Implement the business logic.

In this example, we will use a .NET project called CountryGrpcClient that calls to the server to search, create or get a list of countries.  The CountryGrpc.proto file (described below) declares the remote procedures.

1. Create a Project

To create a gRPC template .NET project, open a terminal and execute the next command:

dotnet new console -o grpc.country.client -n CountryGrpcClient

The output is something like this:

Here:

  • -o parameter is used to define the project directory name: grpc.country.client.
  • -n parameter is used to define the project name: CountryGrpcClient.

2. Add the gRPC Dependencies to the Project

dotnet add CountryGrpcClient.csproj package Grpc.Net.Client --version '2.47.0'
dotnet add CountryGrpcClient.csproj package Grpc.Tools --version '2.47.0'
dotnet add CountryGrpcClient.csproj package Google.Protobuf --version '3.21.5'

You can see that the entry ItemGroup in your CountryGrpcClient.csproj has changed to something like this:

3. Add the Proto File in the Project

In your project directory, create a folder called Protos with a file CountryGrpc.protos in it. 

mkdir ./Protos
touch ./Protos/CountryGrpc.proto

In this step, I’ll use the same proto file created in the previous article in which we created a gRPC server.

Copy the next lines in the CountryGrpc.proto file created previously.

syntax = "proto3";
/*The Proto file that has Empty message definition*/
import "google/protobuf/empty.proto";
// Defining the namespace in which the generate classes will be 
option csharp_namespace = "Sumaris.Grpc.Services";
// The service name will be used by the compiler when generate the base classes
// Here I declare five procedure
service CountryService{
	//Server streaming RPC
    rpc getAllCountries(google.protobuf.Empty)
        returns (stream Country);
	// Unitary RPC
    rpc listAllCountries(google.protobuf.Empty)
        returns ( CountryList);
    // Unitary RPC
    rpc findCountryByName( FindCountryByNameRequest )
        returns (FindCountryByNameResponse);
	// Unitary RPC
    rpc createCountry (CountryCreateRequest)
        returns (CountryCreateRespopnse);
	// Bidrectional streaming RPC
    rpc findCountriesByNames( stream FindCountryByNameRequest)
        returns (stream Country);
}

message Country{
    string name=1;
    string capitalCity=2;
    float area=3;
}
message CountryList{repeated Country country = 1;}
message FindCountryByNameRequest{string name=1;}
message FindCountryByNameResponse{Country country=1;}
message CountryCreateRequest{ Country country=1;}
message CountryCreateRespopnse{bool created=1;}

4. Register the Created Proto File and Compile the Project

Add the next lines in your configuration project file, CountryGrpcClient.csproj:

<ItemGroup>
    <Protobuf Include="./Protos/CountryGrpc.proto" GrpcServices="Client" />
</ItemGroup>

Open a terminal, move into the project, and execute the next command to compile the project:

 The build process creates two files, CountryGrpc.cs and CountryGrpcGrpc.cs, in the obj/Debug/net6.0/Protos path.  The file CountryGrpcGrpc.cs contains the class that we will use as a client to interact with the gRPC server.

5. Implement the Business Logic

Open an IDE to edit the Program.cs file and add the code to call the server:

using System.Threading.Tasks;
using Grpc.Net.Client;
using Sumaris.Grpc.Services;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;

var countryNames = new string[] { "Perú", "Ecuador", "Chile", "Brasil", "Argentina", "Venezuela" };
// See https://aka.ms/new-console-template for more information
using var channel =
    GrpcChannel.ForAddress("http://localhost:5000");
CountryService.CountryServiceClient? client =
    new CountryService.CountryServiceClient(channel);
// Consume unitary
// rpc listAllCountries(google.protobuf.Empty) returns ( CountryList);
var reply = await client.listAllCountriesAsync(new Empty());
Console.WriteLine("Hello, World!");
Console.WriteLine("Greeting: " + reply.Country);

// Consume stream server
//rpc getAllCountries(google.protobuf.Empty) returns (stream Country);
printServerStream();

// Consume unitary
//rpc findCountryByName(FindCountryByNameRequest ) returns(FindCountryByNameResponse);

// Consume client streaming server streaming
//rpc findCountriesByNames( stream FindCountryByNameRequest)  returns (stream Country);
var asyncDuplexStreamingCall = client.findCountriesByNames();
callStreamFromClient(asyncDuplexStreamingCall.RequestStream);
getStreamFromServer(asyncDuplexStreamingCall.ResponseStream);

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

async void printServerStream()
{
    var reply2 = client.getAllCountries(new Empty()).ResponseStream;
    Console.WriteLine("Greeting2: ");
    while (await reply2.MoveNext())
    {
        Console.Write(reply2.Current);
    }
    Console.WriteLine();
}
async void callStreamFromClient(IClientStreamWriter<FindCountryByNameRequest> request)
{
    foreach (var countryName in countryNames)
    {
        var d = new FindCountryByNameRequest(); 
        d.Name = countryName;
        await request.WriteAsync(d);
    }
    await request.CompleteAsync();
}
async void getStreamFromServer(IAsyncStreamReader<Country> response)
{
    await foreach(var p in response.ReadAllAsync())
    {
        Console.Write($">>{response.Current}");
    }
    Console.WriteLine("Terminando ...");
}

In the next images, I want to explain a little more about how you consume the remote procedure. 

The step marked as 1 lets you create the “client” object. You need only one client to interact with the server.

From this point, you use the “client” to call each one of the remote procedures as you can see in step 2. In this case, the client is calling a server streaming RPC called getAllCountries. The server sends the data to the client asynchronously and in streaming fashion (step 3). The client reads the data in streaming fashion too until the server finishes the sending (step 4).

Now that we saw how you can call a streaming server gRPC, I will show you how you can call a bidirectional streaming gRPC.

In this case, we use the same “client” object created previously, step 1, and call the remote bidirectional streaming gRPC. In this example, it is findCountriesByName, which returns an object AsyncServerStreamingCall (step 2). This object wraps two objects: the RequestStream that is used to send streaming data to the server (step 3), and the ResponseStream that is used to get the streaming data returned by the server (step 5). 

The server processes each incoming object sent by the client in the transmission stream, applies its business logic, and asynchronously writes its response to the transmission return stream as we can see in step 4. The client reads the incoming responses sent by the server using the IAsyncStreamReader, as you can see in step 5.

When the client has no more data to send to the server, it must notify the server (red box in step 3), so that the server finishes its process of reading the request asynchronously and can leave the foreach in step 4. In this case, the server ends its process and notifies the client that there is no more data to read. At this point, the client exits the foreach in step 5.

This completes bidirectional transmission.

Now that I have shown you a bidirectional streaming gRPC, you can use it to implement a client streaming gRPC.

Conclusion

We  use the Protocol buffer files to generate the client implementation in .NET and the async/await to manage streaming server or client.  We saw how we can call a streaming server and bidirectional streaming gRPC.

Feel free to let me know if you have any questions or feedback. 

Thank you!

Source: dzone.com