Faster Request-Response in .NET with gRPC

Leveraging Protocol Buffers over HTTP/2 to Build Faster, Strongly Typed APIs

Roko Kovač
5 min readAug 21, 2023

Introduction

REST is the most widely adopted architectural style for building APIs. It produces simple, human-readable interfaces that are easy to understand, implement and consume. However, it is not without shortcomings.

In this article, I will briefly go over those shortcomings, how gRPC addresses them, and then show you how to implement a gRPC client and server in .NET.

REST vs gRPC

First, let’s briefly go over the main differences between the two.

REST is an architectural style that enables communication using JSON messages over HTTP/1.1.

REST communication

gRPC is a protocol that enables communication using Protocol Buffer messages over HTTP/2.

gRPC communication

Protocol buffers are a strongly typed, binary format, while JSON is a text-based format. This makes it 7 to 10 times faster to serialize/deserialize and produces a much smaller payload.

This significant performance improvement comes at the expense of the messages not being human-readable and the lack of browser support.

This is why gRPC is mostly used for communication between services.

In addition to unary calls (request-response), gRPC also supports streaming. However, this is beyond the scope of this article.

Creating a gRPC Server

Let’s move onto the implementation.

First, we will create a new gRPC Server project. We will use the grpc template provided by Microsoft and name it Grpc.Server.

dotnet new grpc -o Grpc.Server

Let’s unpack what exactly this template contains.

.csproj

The project references the Grpc.AspNetCore package, which implicitly brings in Google.Protobuf (the runtime library), Grpc.Tools (ProtoBuf compilers), the standard ASP.NET Core libraries, as well as some extension methods to make registering gRPC services easy.

It also references the greet.proto file, and sets the GrpServices options to “Server” to stop the tool from generating Client files since we don’t need them.

<ItemGroup>
<Protobuf Include="Protos\greet.proto" GrpcServices="Server" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.49.0" />
</ItemGroup>

appSettings

As gRPC uses HTTP/2, an entry is added in the appSettings that configures Kestrel endpoints’ default protocol to HTTP/2.

ProtoBuf File

As we mentioned earlier, gRPC is a protocol that uses strongly typed ProtoBuf contracts. These Protocol Buffer contracts are defined in .proto files.

In the Protos folder, there is a greet.proto file.

syntax = "proto3";

option csharp_namespace = "Grpc.Server";

package greet;

service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
string name = 1;
}

message HelloReply {
string message = 1;
}

The syntax should be reasonably clear for any C# developer.

There’s a declaration specifying the version of the proto file, a C# specific namespace option (more on that later), and a definition of the methods and messages which resemble request, response, and entity DTOs in a REST API.

One thing that stands out is that each field has a unique id. In ProtoBuf, these are called field numbers. They are used for more efficient encoding while allowing for APIs to evolve without breaking old versions, as long as you don’t reuse or change the IDs in the new versions.

GreeterService

In GreeterService.cs, we inherit from the GreeterBase and override the implementations for our service’s methods.

using Grpc.Core;

namespace Grpc.Server.Services;

public class GreeterService : Greeter.GreeterBase
{
public override Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
{
return Task.FromResult(new HelloReply
{
Message = "Hello " + request.Name
});
}
}

As you can see, most of the code is hidden in the Base class.

But where does this base class come from?

Whenever you build your project, the Grpc Tools will scan for any changes within the .proto files that are included in your project and generate C# code according to the definitions in them.

In our case, the ProtoBuf compiler reads our greet.proto and generates a Greeter.Greeter service base class.

You can see it in action if you inspect the build log:

>C:\Users\Roko\.nuget\packages\grpc.tools\2.49.0\tools\windows_x64\protoc.exe 
--csharp_out=obj\Debug\net7.0\Protos
--plugin=protoc-gen-grpc=C:\Users\Roko\.nuget\packages\grpc.tools\2.49.0\tools\windows_x64\grpc_csharp_plugin.exe
--grpc_out=obj\Debug\net7.0\Protos
--grpc_opt=no_client
--proto_path=C:\Users\Roko\.nuget\packages\grpc.tools\2.49.0\build\native\include
--proto_path=.
--dependency_out=obj\Debug\net7.0\12fbc7ffe642ca7e_greet.protodep
--error_format=msvs Protos\greet.proto

In your build folders, it generates two folders with partial implementations of the base class. These files should never be manually modified or included in version control. The only thing you should ever change is the .proto file.

The program.cs

In the program.cs file, we call a few extension methods.

using Grpc.Server.Services;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddGrpc();

var app = builder.Build();
app.MapGrpcService<GreeterService>();
app.Run();

The AddGrpc() method adds routing, options, and everything required to handle Grpc services.

The MapGrpcService adds route mapping to the GreeterService we implemented.

This is everything you need for a simple gRPC server.

Creating a gRPC Client

To consume the API, we will create a simple console application and install the required packages.

dotnet new console -o Grpc.Client
cd Grpc.Client
dotnet add package Grpc.Net.Client
dotnet add package Google.Protobuf
dotnet add package Grpc.Tools

We will copy over the Proto folder with our greet service definition. We will also reference it in our .csproj, but this time we will make sure only the Client is generated.

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

If you build the project now, you can see from the build log that the Client has been generated.

Grpc.Client -> C:\...\Grpc.Client\bin\Debug\net7.0\Grpc.Client.dll

Let’s use it to send a request.

using Grpc.Net.Client;
using Grpc.Server; // defined in greet.proto csharp_namespace option

var channel = GrpcChannel.ForAddress("http://localhost:5143");
var client = new Greeter.GreeterClient(channel);

var reply = client.SayHello(new HelloRequest { Name = "World" });
Console.WriteLine("Greeting: " + reply.Message);

Make sure the server is running and the address is right.

If you run the client now, you should see the following in the console:

Greeting: Hello World

Using a gRPC Client with Dependency Injection

In a real-world application, you will probably want to inject the client instead of instantiating inside a class that uses it, like in the following example.

using Grpc.Server;

namespace Grpc.Client;

public class GreeterService
{
private readonly Greeter.GreeterClient _client;

public GreeterService(Greeter.GreeterClient client)
{
_client = client;
}

public async Task<string> SayHelloAsync(string name)
{
var reply = await _client.SayHelloAsync(new HelloRequest { Name = name });
return reply.Message;
}
}

To add the client to the Service collection, The package Grpc.Net.ClientFactory provides an easy-to-use extension method.

dotnet add package Grpc.Net.ClientFactory

The usage resembles an HttpClientFactory.

var services = new ServiceCollection();
services.AddTransient<GreeterService>();
services.AddGrpcClient<Greeter.GreeterClient>(o =>
{
o.Address = new Uri("https://localhost:5143");
});
public class GreeterService
{
private readonly Greeter.GreeterClient _client;

public GreeterService(Greeter.GreeterClient client)
{
_client = client;
}

public async Task<string> SayHelloAsync(string name)
{
var reply = await _client.SayHelloAsync(new HelloRequest { Name = name });
return reply.Message;
}
}

You can find the full source code on my GitHub.

Conclusion

I have shown you how to achieve faster Request-Response communication in .NET using gRPC.

gRPC has much more to offer, and some of the features will be explored in future articles.

Any thoughts? Leave a comment.

If you liked the article, consider subscribing to be notified when I post more.

--

--