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.
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
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.