Build High Performance Services using gRPC and .NET7

Get 800% better performance than .NET6

Hammad Abbasi
Geek Culture
12 min readNov 14, 2022

--

Photo by AltumCode on Unsplash

.NET 7 is officially out with Standard Term Support that’ll be supported for 18 months. A number of exciting new features are included, including performance upgrades for Web API, gRPC, ASP.NET, and C#11.

The following topics are covered in this article:

  1. Performance Improvement in .NET 7.
  2. gRPC JSON Transcoding.
  3. Create a gRPC service in .NET 7.
  4. Consuming a gRPC Service Using Postman.
  5. Using Server reflection and Postman
  6. Add Swagger Specification.

In addition to discussing the new features of gRPC in .NET 7, we will also implement a real-world microservice capable of streaming 5 million records within a minute.

Note: If you’re unfamiliar with gRPC Services in .NET, take a look at the following article first.

Here is a quick recap:

  • gRPC is a popular open-source RPC framework developed by CNCF.
  • As a contract-first, language-independent framework, client and server must agree on the contents and delivery manner of messages — Contracts are defined in .proto files, which are then used to generate code using .NET7’s tooling.
  • On a single tcp connection, HTTP/2 supports multiplexing, where you can send multiple requests at the same time.
  • Additionally, gRPC supports streaming of data where a server can send multiple responses to the client at the same time, and vice versa.

What’s new in .NET 7?

1. Performance Improvements

In order for gRPC to support multiplexing, HTTP/2 is required. However, there was a known problem with Kestrel’s implementation of HTTP/2 that caused a bottleneck during the writing of responses over HTTP/2 when the connection was busy. It occurs when you have multiple requests running simultaneously on the same TCP connection but only one thread has the ability to write to the connection at a time. This was done with thread locks in .NET 6, which caused lock contention.

NET 7 uses a clever approach to resolve this bottleneck by implementing a queue, which notifies all other threads when a write has been completed, allowing them to wait for the write to complete. Therefore, performance improved greatly, and CPU resources were better utilized — there is no need to fight over locks anymore.

.NET gRPC team’s benchmarks showed that server streaming was improved by 800%.

  • .NET 6–0.5M RPS
  • .NET 7–4.5M RPS

HTTP/2 Upload Speed

A 600% reduction in latency is achieved by increasing buffer size. .NET 7 reduces uploading a 100MB file from 26.9 seconds to 4.3 seconds in comparison with .NET 6.

.NET 7 gRPC performance now exceeds popular frameworks like Rust, Go and C++.

https://github.com/Lesnyrumcajs/grpc_bench

2. gRPC JSON Transcoding

.NET7 provides an extension for ASP.NET Core gRPC in order to enable gRPC services to be exposed as RESTful Web services. You can now call gRPC methods over HTTP without any duplication.

gRPC JSON Transcoding supports:

  • HTTP Verbs
  • URL parameter bindings
  • JSON requests/response

In this extension, HTTP verbs are mapped to gRPC services by using the concept of protobuf annotations and the extension runs inside ASP.NET Core applications, which then deserialize JSON into protobuf messages and call the gRPC service directly, instead of having to write their own gRPC client applications.

We’ll look at how to implement this in the next section.

3. Open API Specification

There is now an Open API specification for gRPC JSON Transcoding in .NET 7 using the following Nuget package:

https://www.nuget.org/packages/Microsoft.AspNetCore.Grpc.Swagger

4. Azure App Service Support

Last but not least, the Azure App Service now supports gRPC in its entirety. This is a great step forward for building and deploying high performance services using gRPC in .NET.

Now that we’ve done talking, let’s implement the gRPC and see what the new features look like.

Source: https://i.pinimg.com/originals/72/f6/fe/72f6fe384180442d9cd835abd4e021d9.jpg

Prerequisite:

The first thing we need to do is fire up Visual Studio and create a new project. We’ll choose “ASP.NET Core gRPC Service” which will create a sample hello world gRPC service.

Image by Author

Make sure to verify that .NET7 is selected.

Image by Author
Image by Author

This will create a ready-to-use gRPC app in protos and the GreeterService in service folders, respectively.

Here’s a greeting.proto file that serves as a contract, defining the messages and services the client will receive.

syntax = "proto3";

option csharp_namespace = "gRPCUsingNET7Demo";

package greet;

// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply);
}

// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

// The response message containing the greetings.
message HelloReply {
string message = 1;
}

Contracts can be thought of as interfaces, and the implementation of these interfaces would be defined by a service, which in our case is GreeterService.cs — this file is where the implementation of the contract will be described.

The GreeterService class is a standard C# class that returns hello to the response. The actual implementation of protobuf is achieved with code-generation and it is abstracted away using GreeterBase. In case you want to know exactly what’s going on under the hood, you can go to GreeterBase and you’ll find all the low level details there.

public class GreeterService : Greeter.GreeterBase
{
private readonly ILogger<GreeterService> _logger;
public GreeterService(ILogger<GreeterService> logger)
{
_logger = logger;
}

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

Code-genreration is a nice feature of .NET 7 that allows you to generate server-side as well as client-side gRPC code. It is possible to change the behavior of the code generation process in the .CS project file (e.g. from server to client) by setting up the code generation settings.

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

Let’s fire up the Kestral and browse the gRPC endpoint in the browser after opening the app.

Image by Author

We cannot access our gRPC service over the web since it requires a gRPC client to be used. However, instead of requiring a gRPC client to be used, what we will do is test this using the popular testing tool Postman. It has recently added support for gRPC requests to its features.

The first step is to open up Postman and create a new gRPC request.

Please enter the server address (where your app is running) in the box below. For example, https://localhost:7211

Postman does not understand how our service works at the moment, so we have a few options. One is to import a .proto file or to use something called “server reflection”. It can be thought of as an OpenAPI specification for gRPC calls.

Enabling Server Reflection in your gRPC Service.

It is very simple to enable server reflection by following the steps below.

  1. Download and install the following nuget package:
Install-Package Grpc.AspNetCore.Server.Reflection -Version 2.49.0

2. In the Program.cs file, you need to register the following service and map this service in our http pipeline, as follows:

builder.Services.AddGrpcReflection();

app.MapGrpcReflectionService();

Now that we have done all that, let’s go back to Postman, and run the app again.

Image by Author

Yay!! We can see our greet.greeter service and its SayHello method

This endpoint can be invoked by clicking the Invoke button with a JSON body (which will be converted to protobuf by Postman).

Image by Author

That’s great! We got the server response within 49ms.

Turn your gRPC service into REST

This section will implement gRPC JSON Transcoding for accessing gRPC over HTTP.

  1. Add the following nuget package to your project:
Install-Package Microsoft.AspNetCore.Grpc.JsonTranscoding -Version 7.0.0

2. Navigate to Program.cs and add the service for JSONTranscoding:

builder.Services.AddGrpc().AddJsonTranscoding();

As a next step, we will add two protofiles to our project.

Image by Author

Here are the links to the proto files:

  1. https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/annotations.proto
  2. https://raw.githubusercontent.com/googleapis/googleapis/master/google/api/http.proto

After adding those files, we need to modify greet.proto and add import “google/api/annotations.proto” so we can annotate our service methods.

  // Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply)
{
option (google.api.http) =
{
get: "/v1/greeter/{name}"
}

};

Basically, we’re adding a route to our RPC method so that it can be invoked as a REST method. Let’s run the app again and execute the endpoint using a browser.

Image by Author

And that’s it! The API now works as a REST-based API, but it is also still available as a gRPC interface. A gRPC response from Postman is shown below.

Image by Author

Add Open API Specification

The purpose of this section is to explain how we can add open API specifications to our application using gRPC.Swagger.

  1. Install the following nuget package:
Install-Package Microsoft.AspNetCore.Grpc.Swagger -Version 0.3.0

2. Register Swagger services and middleware as follows.

   builder.Services.AddGrpcSwagger();
builder.Services.AddSwaggerGen( c=>
{
c.SwaggerDoc("v1",
new Microsoft.OpenApi.Models.OpenApiInfo { Title = "gRPC using .NET 7 Demo", Version = "v1" } );
});

In the end, your program.cs should look like this:

using gRPCUsingNET7Demo.Services;

namespace gRPCUsingNET7Demo
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);

// Additional configuration is required to successfully run gRPC on macOS.
// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682

// Add services to the container.
builder.Services.AddGrpc().AddJsonTranscoding();
builder.Services.AddGrpcReflection();
builder.Services.AddGrpcSwagger();

builder.Services.AddSwaggerGen( c=>
{
c.SwaggerDoc("v1",
new Microsoft.OpenApi.Models.OpenApiInfo { Title = "gRPC using .NET 7 Demo", Version = "v1" }

});
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "gRPC using .NET7 Demo");
}
);
// Configure the HTTP request pipeline.
app.MapGrpcService<GreeterService>();
app.MapGrpcReflectionService();
app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
app.Run();
}
}
}

Invoke the Swagger endpoint after launching the app. https://localhost:7211/swagger/index.html

Image by Author

You can try it out invoke the endpoint like any Restful API.

Image by Author

Following this section, we will demonstrate how to stream 5M records (around 600MB of data) to the client using gRPC server streaming.

gRPC Server Streaming

In server streaming, the gRPC client sends a request and gets a stream of responses. The client reads these responses until all messages have been delivered. gRPC ensures message ordering.

Use this sample CSV file as an example.

That CSV file contains approximately 5 million sales records, so it would be impossible to deliver them all in one call.

Furthermore, traditional REST-based paging involves multiple client requests and requires back-and-forth communication between client and server.

gRPC Server streaming is an excellent solution to this problem.

  • Clients will simply call the service method.
  • The CSV file will be read line-by-line, converted to the proto model, and sent back to the client using StreamReader.
  • A stream of responses will be sent to the client.

We will start by defining a proto file:

Protos-> sales.proto

syntax = "proto3";
import "google/protobuf/timestamp.proto";
csharp_namespace = "gRPCUsingNET7Demo";

package sales;

service SalesService {
rpc GetSalesData(Request) returns (stream SalesDataModel) {}
}

message Request{
string filters=1;

}

message SalesDataModel {
int32 OrderID = 1;
string Region = 2;
string Country = 3;
string ItemType=4;
google.protobuf.Timestamp OrderDate=5;
google.protobuf.Timestamp ShipDate=6;
int32 UnitsSold=7;
float UnitCost=8;
float UnitPrice=9;
int32 TotalRevenue=10;
int32 TotalCost=11;
int32 TotalProfit=12;
}

With the stream keyword, we can specify that the SalesDataModel will be delivered as a stream.

Our next step is to add a new service — SalesDataService.cs in the following way:

using Grpc.Core;
using gRPCUsingNET7Demo;

namespace gRPCUsingNET7Demo.Services
{
public class SalesDataService : SalesService.SalesServiceBase

{

public override async Task
GetSalesData(Request request,
IServerStreamWriter<SalesDataModel> responseStream, ServerCallContext context)
{

using (var reader = new StreamReader(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data", "sales_records.csv")))
{
string line; bool isFirstLine = true;
while ((line = reader.ReadLine()) != null)
{
var pieces = line.Split(',');

var _model = new SalesDataModel();

try
{
if (isFirstLine)
{
isFirstLine = false;
continue;
}

_model.Region = pieces[0];
_model.Country = pieces[1];

_model.OrderID = int.TryParse(pieces[6], out int _orderID) ? _orderID : 0;
_model.UnitPrice = float.TryParse(pieces[9], out float _unitPrice) ? _unitPrice : 0;

_model.ShipDate = Google.Protobuf.WellKnownTypes.Timestamp.FromDateTime
((DateTime.TryParse(pieces[7], out DateTime _dateShip) ? _dateShip : DateTime.MinValue).ToUniversalTime());

_model.UnitsSold = int.TryParse(pieces[8], out int _unitsSold) ? _unitsSold : 0;

_model.UnitCost = float.TryParse(pieces[10], out float _unitCost) ? _unitCost : 0;

_model.TotalRevenue = int.TryParse(pieces[11], out int _totalRevenue) ? _totalRevenue : 0;
_model.TotalCost = int.TryParse(pieces[13], out int _totalCost) ? _totalCost : 0;

await responseStream.WriteAsync(_model);

}

catch (Exception ex)
{
throw new RpcException(new Status(StatusCode.Internal, ex.ToString()));
}

}
}

}

}
}

This service implements the SalesServiceBase class which is auto-generated by .NET7 tooling using the proto file.

It simply overrides GetSalesData to read the data from the file line by line and return it as a stream.

await responseStream.WriteAsync(_model);

Let’s build the project and run the app.

The application is running as expected. To obtain the stream of orders from the server, we will need to create a separate RPC client, which is described in the next section.

Creating a gRPC Client using .NET7

Let’s create a new console app in your solution and add the following packages to it

<PackageReference Include="Google.Protobuf" Version="3.21.9" />
<PackageReference Include="Grpc.Net.ClientFactory" Version="2.49.0" />
<PackageReference Include="Grpc.Tools" Version="2.40.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
  1. Make sure that the Protos folder is added and the sales.proto file is copied there.
  2. In order to generate gRPC classes for the client side, you will need to modify the .csproj file.
<ItemGroup>
<Protobuf Include="Protos\sales.proto" GrpcServices="Client" />
</ItemGroup>

3. The project should be saved and built (so that client-side code is generated as well)

4. The first step is to open Program.cs and create a channel for your gRPC service.

var channel = GrpcChannel.ForAddress("https://localhost:7211");

5. Create a new object of SalesService (created by using the gRPC tooling) as follows:

var client = new SalesService.SalesServiceClient(channel);

6. The service method should be invoked as follows:

using var call = client.GetSalesData(new Request { Filters = "" });

7.Our code simply calls ReadAllAsync on the server to retrieve the stream and then prints the output on the console as soon as it is received.

await foreach (var each in call.ResponseStream.ReadAllAsync())
{
Console.WriteLine(String.Format("New Order Receieved from {0}-{1},Order ID = {2}, Unit Price ={3}, Ship Date={4}", each.Country, each.Region, each.OrderID, each.UnitPrice,each.ShipDate));
Count++;
}

This is how complete implementation looks like

using Grpc.Core;
using Grpc.Net.Client;
using gRPCUsingNET7Demo;

namespace gRPCClient
{
internal class Program
{
static async Task Main(string[] args)
{
var channel = GrpcChannel.ForAddress("https://localhost:7211");
int Count = 0;
var watch = System.Diagnostics.Stopwatch.StartNew();
try
{
var client = new SalesService.SalesServiceClient(channel);

using var call = client.GetSalesData(new Request { Filters = "" }
, deadline: DateTime.UtcNow.AddMinutes(10)
);

await foreach (var each in call.ResponseStream.ReadAllAsync())
{

Console.WriteLine(String.Format("New Order Receieved from {0}-{1},Order ID = {2}, Unit Price ={3}, Ship Date={4}", each.Country, each.Region, each.OrderID, each.UnitPrice, each.ShipDate));
Count++;

}
}
catch (RpcException ex) when (ex.StatusCode == StatusCode.DeadlineExceeded)
{
Console.WriteLine("Service timeout.");
}

watch.Stop();

Console.WriteLine($"Stream ended: Total Records:{Count.ToString()} in {watch.Elapsed.TotalMinutes} minutes and {watch.Elapsed.TotalSeconds} seconds.");
Console.Read();

}
}
}

As you can see in the example above, the service method invocation is done with the help of a deadline. You can specify the duration of a call using deadlines, so you can specify how long the call should last.

using var call = client.GetSalesData(new Request { Filters = "" }
, deadline: DateTime.UtcNow.AddMinutes(10)
);

The client now allows you to view the incoming messages from the gRPC service.

Image by Author

Conclusion:

The purpose of this article is to provide information about the performance enhancements that have been added to the gRPC .NET 7 framework, including a gRPC JSON transcoding feature, the OpenAPI specification, and server reflection features, as well as the new performance improvements. The article also explains how gRPC Server streaming can be used to create high performance services capable of processing and delivering millions of records in no time.

The source code can be downloaded from this repository.

--

--

Hammad Abbasi
Geek Culture

Innovating Enterprise Applications with AI & LLM | Solution Architect | Tech Writer & Innovator | Bringing Ideas to Life using Next-Gen Tech Innovations