gRPC in .NET 6

2022, Jun 11

gRPC is a modern open-source high-performance Remote Procedure Call (RPC) framework that can run in any environment. It uses Protobuf as its Interface Definition Language (IDL).

This is now becoming really popular with the advent of Cloud Native apps.

The project

The gRPC is a CNCF (Cloud Native Computing Foundation)1 project that was accepted to CNCF on February 16, 2017, and is at the Incubating project maturity level.

Incubating projects are considered stable and used successfully in production.

Going backwards, this initiative started with Google, to make an open-source project out of an existing RPC infrastructure called Stubby, used to connect microservices in their own platform.

For more details and background on gRPC, check this link.

HTTP/2

Google is the father of HTTP/2, an evolution of an experimental protocol called SPDY.

HTTP/2 introduced a new binary framing layer, which dictates how the messages are encapsulated and transferred between client and server.

Source: web.dev Source: https://web.dev/performance-http2/

This makes HTTP/2 compacted (binary) and faster (TCP) when compared with HTTP/1.x, which only leverages simple text and HTTP as a message transporter.

Terminology

  • Stream: A bidirectional flow of bytes within an established connection, which may carry one or more messages. Different from the HTTP/1.x that can only transmit a single request or response within an established connection.
  • Message: A complete sequence of frames that map to a logical request or response message.
  • Frame: The smallest unit of communication, each containing a frame header, which identifies the stream to which the frame belongs.

Source: web.dev Source: https://web.dev/performance-http2/

In summary:

  • Within a single TCP connection many streams are transmitted.
  • Within each stream, there can be many messages.
  • The messages are a sequence of frames.
  • Frames contain the information transmitted.

Characteristics

Multiplexing

With HTTP/1.x, if the client wants to make multiple parallel requests to improve performance, then multiple TCP connections must be used.

Source: web.dev Source: https://web.dev/performance-http2/

The new binary framing layer in HTTP/2 removes these limitations and enables full request and response multiplexing.

It also resolves the head-of-line blocking problem2 found in HTTP/1.x and eliminates the need for multiple connections. As a result, this makes our applications faster, simpler, and cheaper to deploy.

Stream prioritization

When the message is split into multiple frames, it can be ordered in a way to optimise performance. The HTTP/2 standard allows each stream to have an associated weight and dependency:

  • Each stream may be assigned an integer weight between 1 and 256.
  • Each stream may be given an explicit dependency on another stream.

Other characteristics

Considering what is important to enabling gRPC, the previous characteristics have enabled the RPC framework. There are other HTTP/2 inherent characteristics that are very well described here, so I'm not extending the explanation of those additional characteristics in this article.

Protobuf

Protocol buffers provide a language-neutral, platform-neutral, extensible mechanism for serializing structured data that generates native language bindings.

It means that you get to create a generic definition of a data contract, using .proto files, that get compiled using a language-specific runtime library as C#/Python and other languages, then generating a language-specific contract to interface with data.

Another important aspect of protocol buffers is the serialization format for data that is written to a file (or sent across a network connection).

Message

The syntax of protobuf messages is simple. Below there are some examples I created.

File: address.proto

syntax = "proto3";

option csharp_namespace = "GrpcServiceSample";

package address;

message Address {
    string number = 1;
    string street_name = 2;
    string suburb = 3;
    string city = 4;
    string country = 5;
}

Note that we are using proto3 as syntax.

In particular on C#, the option csharp_namespace was set as language specific, and it will be translated into a C# namespace when compiled.

Packages help organise messages. You cannot have the same message name duplicated on a package, but you can create the same message name in a different package.

Messages have attributes, and they need to be numerically identifiable.

File: user.proto

syntax = "proto3";

option csharp_namespace = "GrpcServiceSample";

package user;

import "protos/address.proto";

service UserService {
    rpc GetUserDetails(UserRequest) returns (UserResponse);
}

message UserRequest {
    int32 user_id = 1;
}

message UserResponse {
    int32 user_id = 1;
    string first_name = 2;
    string last_name = 3;
    address.Address address = 4;
}

We can use import to reference other proto messages.

Services need to be specified in proto files, for rpc calls between client and server.

For further details on creating proto files, the Protobuf style guide contains recommendations / best practices.

Running the service

Consider the VStudio 2022 template:

ASP.NET Core gRPC Service

Server:

using GrpcServiceSample.Interceptors;
using GrpcServiceSample.Services;

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(options =>
{
    options.Interceptors.Add<ServerLoggerInterceptor>();
});

var app = builder.Build();

// Configure the HTTP request pipeline.
app.MapGrpcService<GreeterService>();
app.MapGrpcService<UserService>();
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();

The UserService is created:

using Grpc.Core;

namespace GrpcServiceSample.Services
{
    public class UserService : GrpcServiceSample.UserService.UserServiceBase
    {
        private readonly ILogger<UserService> _logger;
        public UserService(ILogger<UserService> logger)
        {
            _logger = logger;
        }

        public override Task<UserResponse> GetUserDetails(UserRequest request, ServerCallContext context)
        {
            return Task.FromResult(new UserResponse
            {
                UserId = 1,
                FirstName = "Bill",
                LastName = "Lumbergh",
                Address = new Address()
                {
                    Number = "1A",
                    StreetName = "Initech Street",
                    Suburb = "Initech Suburb",
                    City = "Austin, Texas",
                    Country = "USA"
                }
            });
        }
    }
}

An interceptor is added to the server, that will capture the request, and log details before the request happens:

using Grpc.Core;
using Grpc.Core.Interceptors;

namespace GrpcServiceSample.Interceptors
{
    public class ServerLoggerInterceptor : Interceptor
    {
        private readonly ILogger _logger;

        public ServerLoggerInterceptor(ILogger<ServerLoggerInterceptor> logger)
        {
            _logger = logger;
        }

        public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(
            TRequest request,
            ServerCallContext context,
            UnaryServerMethod<TRequest, TResponse> continuation)
        {
            _logger.LogInformation($"Starting receiving call. Type: {MethodType.Unary}. " +
                                   $"Method: {context.Method}.");
            try
            {
                return await continuation(request, context);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, $"Error thrown by {context.Method}.");
                throw;
            }
        }
    }
}

Then add a new Console Application, and make sure to service reference the gRPC server:

Service Reference

Generate the client:

Service Reference

In the end, you should have referenced both address.proto and user.proto files:

Service Reference

Then to make a request to the server, this is the client implementation:

using System.Text.Json;
using Grpc.Net.Client;
using GrpcServiceSample;

var channel = GrpcChannel.ForAddress("https://localhost:7147");
var client = new UserService.UserServiceClient(channel);

var userDetails = await client.GetUserDetailsAsync(new UserRequest() {UserId = 1});

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

The channel is created, and then the UserService can be requested.

This is what the solution looks like:

Solution

Check this sample in the PlayGoKids repository.

Where to use

gRPC is well suited to the following scenarios3:

  • Microservices: gRPC is great for lightweight microservices where efficiency is critical because it is designed for low latency and high throughput communication.
  • Point-to-point real-time communication: gRPC services can push messages in real-time without polling because it has excellent support for bi-directional streaming.
  • Multi-language environments: gRPC tooling supports all popular development languages.
  • Network constrained environments: gRPC messages are serialized with Protobuf, a lightweight message format.
  • Inter-process communication (IPC): IPC transports such as Unix domain sockets and named pipes can be used with gRPC to communicate between apps on the same machine.