Sample build from scratch of a gRPC client-server application with C# and dotnet core.

Infrastructure

Here's how the folder structure will be:

  • Root folder
    • GrpcServer (dotnet core console app)
    • GrpcClient (dotnet core console app)
    • Message (dotnet core class lib)
    • gencert.bat (bash script to generate ssl certificates)

Install OpenSSL

We will need to make use of OpenSSL tool to generate self-signed certificates. I am writing this sample in my windows 10 machine, so I've downloaded OpenSSL installer from https://slproweb.com/products/Win32OpenSSL.html

Make note of the installing path, You will need it in the next step.

Bash script to generate self-signed SSL certificates

This StackOverflow answer contains a batch script that can be used to generate the certificates. That is a great answer if you are using that script make sure to upvote the answer to give credit to the author.

Copy the bash script and save it as gencert.bat. The author has installed OpenSSL in c:\OpenSSL-Win64\. If you have used another location make sure to update the 3'rd line of the script so it matches the location of your config file. Here is my gencert.bat file content:

REM original material is taken from Stack Overflow (https://stackoverflow.com/a/37739265/6089658)
@echo off
set OPENSSL_CONF=C:\Program Files\OpenSSL-Win64\bin\openssl.cfg

echo Generate CA key:
openssl genrsa -passout pass:1111 -des3 -out ca.key 4096

echo Generate CA certificate:
openssl req -passin pass:1111 -new -x509 -days 365 -key ca.key -out ca.crt -subj  "/C=US/ST=CA/L=Cupertino/O=YourCompany/OU=YourApp/CN=MyRootCA"

echo Generate server key:
openssl genrsa -passout pass:1111 -des3 -out server.key 4096

echo Generate server signing request:
openssl req -passin pass:1111 -new -key server.key -out server.csr -subj  "/C=US/ST=CA/L=Cupertino/O=YourCompany/OU=YourApp/CN=%COMPUTERNAME%"

echo Self-sign server certificate:
openssl x509 -req -passin pass:1111 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt

echo Remove passphrase from server key:
openssl rsa -passin pass:1111 -in server.key -out server.key

echo Generate client key
openssl genrsa -passout pass:1111 -des3 -out client.key 4096

echo Generate client signing request:
openssl req -passin pass:1111 -new -key client.key -out client.csr -subj  "/C=US/ST=CA/L=Cupertino/O=YourCompany/OU=YourApp/CN=%CLIENT-COMPUTERNAME%"

echo Self-sign client certificate:
openssl x509 -passin pass:1111 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt

echo Remove passphrase from client key:
openssl rsa -passin pass:1111 -in client.key -out client.key

Code for this sample project

Generate certificates

Open a Powershell window in the same folder as gencert.bat file and execute: .\gencert.bat

You should have now 8 new files:

  • two for the certificate authority
  • three for client certificate
  • three for server certificate

Visual Studio solution with projects

Create a new visual studio solution in the root folder (that's the same path as gencert.bat) and add the following .Net Core projects:

  • Message (Class Library)
  • GrpcServer (Console App)
  • GrpcClient (Console App)

Inside GrpcServer project, create a new folder named Certs, copy inside that folder certificate authority and server certificate files, select all, and set Copy to output directory as Copy if newer. This will make the path to the certificates easier to work with.

Repeat this step for the GrpcClient application, now copy certificate authority and client certificate files instead of server ones. Make sure you've set Copy to output directory as Copy if newer.

Hypothetical example

Chatty microservices is one area where gRPC shines at, so we will take a hypothetical example where we need to transfer 500* rows of locations with these properties:

  • DeviceId -> number
  • Latitude -> string
  • Longitude -> string

I generated those data from generatedata.com/

Protocol Buffers

Protocol buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data. We will make use of proto files to let the protoc compiler know about the structure of our data. All we will do is build a messages.proto file that will show how a message should look like, and what actions will our API make available. Then we will see how we get c# ready to use classes for free :).

To have syntax highlight for protobuf I've installed Protobuf Language Service visual studio extension

In Message project, create a new file named messages.proto

We will start by setting the version 3 for syntax, and a NameSpace for C#. Then we define how Invoice object should look like. One caveat in here is that you can't request/respond with a void, every request/response should have a non-primitive type, so we will define the message structure (imagine c# model classes) for all request/response cases.

syntax = "proto3";

option csharp_namespace = "Messages";

/* base type for location */
message Location{
    int32 deviceId = 1;
    string  latitude = 2;
    string longitude = 3;
}


/* client asking for all locations */
message GetAllRequest {}

/* response with all locations */
message GetAllResponse {
    Location location = 1;
}


/* service: this denotes the services we will serve, which will make use of previous message type declarations */
service InvoiceService{
    /* this will instruct the compiler that InvoiceService will have a method named GetAll, which takes as argument a GetAllRequest and returns a stream of GetAllResponse */
    rpc GetAll(GetAllRequest) returns (stream GetAllResponse);
}

Message class library project

Install Google.Protobuf, Grpc and Grpc.Tools nuget packages.

Explicitly include proto file by editing csproj. Here is Messages.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Google.Protobuf" Version="3.7.0" />
    <PackageReference Include="Grpc" Version="1.19.0" />
    <PackageReference Include="Grpc.Tools" Version="1.19.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
    </PackageReference>

    <Protobuf Include="messages.proto" />
  </ItemGroup>
</Project>

At this point a project build will generate necessary C# code, compiled. you can view it under .\obj\debug\netcore2.2\ as MessagesGrpc.cs and Messages.cs

GrpcServer project

Set this project as startup project, add a reference to Messages project then install Google.Protobuf and Grpc NuGet packages.

Create a public class named LocationsService and inherit from Messages.InvoiceService.InvoiceServiceBase. The name is easy to find with the help of intellisense, the same goes for methods that can be invoked from this service.

image with intellisense help from dekstop

I've added a Repository class with a public collection property. Suppose this is some sort of DB connection or something that returns real and useful data.

Here we will override the GetAll method, and we will stream all entries from repository class. Here's how my LocationService.cs looks like:

public class LocationsService : Messages.InvoiceService.InvoiceServiceBase
{
    private readonly Repository repository;
    public LocationsService()
    {
        repository = new Repository();
    }

    public override async Task GetAll(GetAllRequest request, IServerStreamWriter<GetAllResponse> responseStream, ServerCallContext context)
    {
        foreach (var item in repository.locationsCollection)
        {
            await responseStream.WriteAsync(new GetAllResponse { Location = item });
        }

        Console.WriteLine("Finished reponse stream");
    }
}

One more thing left to be done in the server side: Server start. For this I've added a method to read and return certificate files (the ones saved in Certs folder), and update the main method to look like the following:

static void Main(string[] args)
{
    const int Port = 50050;
    const string host = "127.0.0.1";
    var servCred = GetCerts(); // read and return certificate files (SslServerCredentials object)

    var server = new Server()
    {
        // setup host, port and server credentials
        Ports = { new ServerPort(host, Port, servCred) },
        // register the service we built earlier
        Services = { Messages.InvoiceService.BindService(new LocationsService()) }
    };

    server.Start();

    Console.WriteLine($"Starting server on port: {Port}\nPress any key to stop");

    Console.ReadKey();
    server.ShutdownAsync().Wait();
}

With this in place, we have an already working gRPC server ready to accept requests from clients and return a correct response (a server stream).

Grpc Client project

We are approaching the end of this example, all we have to do now is connect to the gRPC server and consume his services.

Install Google.Protobuf and Grpc NuGet packages, add a reference to Message project. Same as GrpcServer project, here I've created a separate method to return credentials. The idea behind this is to keep the important code clean.

To keep the code simple and focus more on the idea then coding structure, I'll make the calling code inside the main method. To do this we are going to need to have async main, and that is supported after C# 7.1. So to set the C# version to the latest available

-> RightClick project node -> Properties -> Build -> Advanced -> C# version 7.3

Note that the certificates in here are generated using MachineName, so pay attention to channelOptions variable.

Here's the complete code for GrpcClient.Program.Main method:

static async Task Main(string[] args)
{
    const int Port = 50050;
    const string Host = "127.0.0.1";

    var creds = GetSslCredentials();

    var PcName = Environment.MachineName;
    var channelOptions = new List<ChannelOption>
        {
            new ChannelOption(ChannelOptions.SslTargetNameOverride, PcName)
        };
    var channel = new Channel(Host, Port, creds, channelOptions);
    var client = new Messages.InvoiceService.InvoiceServiceClient(channel);

    Console.WriteLine("GrpcClient is ready to issue requests. Press any key to start");
    Console.ReadKey();

    using (var call = client.GetAll(new Messages.GetAllRequest()))
    {
        var responseStream = call.ResponseStream;
        while (await responseStream.MoveNext())
        {
            Console.WriteLine(responseStream.Current.Location);
        }
    }

    Console.WriteLine("\n\nFinished GetAll request with server-stream demo. Press any key to close this window");
    Console.ReadKey();
}

Test Client-Server C# demo with gRPC

To test this solution in visual studio: Set GrpcServer project as Startup Project and hit Debug, then right-click GrpcClient project, choose Debug and Start new instance

grpc test with C# Server Client

You can find the full source for this project in this GitHub repo

Conclusion

Grpc shines in many areas, what I like the most is performance, TypeSafety, and intellisense, ability to work in parallel for client and server applications, ready to use code for top 10 used languages etc.. among all these great features grpc comes with a few caveats too, it's not intended to be used for browser clients, you have to buy certificates and you'll need to spend some time to test your server since you can't just curl a test call for it.