DotNetT2 Light_1200x303
gRPC is a high-performance RPC framework with pluggable authentication and load balancing features. In this post, you will learn about gRPC and protocol buffers, how to build gRPC services in .NET Core/5 using C# and also how to create gRPC clients.

gRPC is a high-performance RPC framework that efficiently allows service-to-service communication within and across data centers. It also supports connecting mobile devices and browser clients to backend services. It was implemented in Google and later open-sourced, and it’s currently a Cloud Native Computing Foundation (CNCF) incubation project. Its features include:

  • Bi-directional streaming
  • Powerful binary serialization
  • Pluggable authentication, load balancing and health check

If you adopt microservice architecture in your organization, gRPC would most likely give a performance boost for the communication between your microservices.

In this post, I’ll show you how to create a gRPC service using .NET Core 3.1 (or later). I will break down some important foundational concepts of gRPC and also include steps for doing those for Mac and VS Code users.

As a prerequisite, I expect some familiarity with C#, ASP.NET Core and either Visual Studio or VS Code.

Create a gRPC Server

We will start by creating a new dotnet project with the gRPC service template.

If you use Visual Studio, create a new project and select the gRPC Service template. Use GrpcAuthor as the name of the project.

If you use VS Code, you’re going to create the project using dotnet CLI and open it in VS Code. Open your command-line application (or Terminal if you’re on Mac) and run the command dotnet new grpc -o GrpcAuthor. Then open the project in VS Code by running the command code -r GrpcAuthor.

The RPC Service Definition

A gRPC client application can directly call a method on a server application as if it is a local object. The client and server applications talk to each other using protocol buffers. Protocol buffers are used as both the Interface Definition Language (IDL) for the service and as its underlying message interchange format.

A service interface is defined in a .proto file using protocol buffer. In it, you specify the methods that can be called remotely with their parameters and return types. The server implements this interface and runs a gRPC server to handle client calls. The client, on the other hand, has a stub (referred to as just a client in some languages) that provides the same methods as the server, and with it the client calls the server methods as if they are local to the application.

After the service is defined, you use the protocol buffer compiler protoc to generate data access/transfer classes from your proto definition file. This file contains the implementation of the messages and methods in the service interface.

The template used to create the project already includes a proto file greet.proto which is in the Protos folder.

syntax = "proto3";

option csharp_namespace = "GrpcAuthor";

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;
}

Let’s break down the proto file in order to understand the protocol buffer syntax.

The Greet Service Interface Definition

syntax = "proto3";

The syntax specifier is used to indicate the version of protocol buffer in use. In this case, proto3, which is the latest version as of the time of this writing and has more features and language support than its predecessor.

option csharp_namespace = "GrpcAuthor";

package greet;

The option csharp_namespace specifier is used to specify the namespace which the generated files will have. It is used to prevent name clashes between protocol message types just like the package specifier. The way the package specifier affects the generated code depends on the language you’re using. For C#, it is used as the namespace unless you provide an option csharp_namespace. In Java, it’s used as the package name unless you specify option java_package in the .proto file.

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

HelloRequest and HelloReply are the data structures that will be used to exchange information between the client and the server. They’re referred to as messages and contain name-value pairs called fields. The number you see in the field definition is a unique number that is used to identify fields when the message is serialized to Protobuf. This is because serializing a small number is faster than serializing the entire field name.

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

This is the service definition and it contains a method SayHello with a parameter and a return type as protocol buffer messages.

In order to generate code for the .proto file, you use the protoc compiler and C# plugin to generate the server or client code. This will be done for you using the Grpc.Tools NuGet package. The needed classes are generated automatically by the build process. It knows how to generate the types through the item group <Protobuf> setting in your .csproj file.

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

The generated code knows how to communicate with other services/clients using protocol buffers. The C# tooling generates the GreeterBase type which will be used as the base class to implement the gRPC service.

There’s an implementation for the Greeter service in Services/GreeterService.cs:

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
        });
    }
}

The service is implemented by inheriting from the Greeter.GreeterBase class. This base class is generated at build time using the greet.proto file.

Create a New gRPC Service

You’ve seen the generated Greeter service from the project template. You’ll create a new service with one RPC method to get a book author by name.

Add a new file to the Protos folder named author.proto, then copy and paste the interface definition below in it.

syntax = "proto3";

option csharp_namespace = "GrpcAuthor";

package author;

service Author {
  rpc GetAuthor (AuthorRequest) returns (AuthorResponse);
}

message AuthorRequest {
  string name = 1;
}

message BookReply {
  string title = 1;
}

message AuthorResponse {
  string name = 1;
  repeated BookReply books_authored = 2;
}

In this service definition, you have an Author service with one RPC method GetAuthor. The AuthorResponse message contains name and books_authored fields. The books_authored field is a composite type of BookReply. The repeated field rule is used to indicate that it’s a collection; therefore, it can be repeated any number of times in a serialized message.

Now that you have the service definition, you need to make your project know about it so that it will generate the necessary code at build time. You will do this by updating the GrpcAuthor.csproj file to include an item group with a <Protobuf> element that refers to the author.proto file.

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

Implement the Service Definition

After creating the service contract, you need to implement it. Create a new file in the Services folder named AuthorService.cs. Copy and paste the code below in it.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Grpc.Core;
using Microsoft.Extensions.Logging;

namespace GrpcAuthor
{
    public class AuthorService : Author.AuthorBase
    {
        private readonly ILogger<AuthorService> _logger;
        private List<AuthorResponse> authors;

        public AuthorService(ILogger<AuthorService> logger)
        {
            _logger = logger;
            authors = new List<AuthorResponse>();

            var antonio = new AuthorResponse { Name = "Antonio Gonzalez" };
            antonio.BooksAuthored.Add(new BookReply { Title = "Much Ado about nothing" });
            antonio.BooksAuthored.Add(new BookReply { Title = "How to do a split"});
            authors.Add(antonio);

            var jack = new AuthorResponse { Name = "Jack Olabisi" };
            jack.BooksAuthored.Add(new BookReply { Title = "Early morning bird" });
            jack.BooksAuthored.Add(new BookReply { Title = "Fly me to Paris"});
            authors.Add(jack);
        }

        public override Task<AuthorResponse> GetAuthor(AuthorRequest request, ServerCallContext context)
        {
            var author = authors.FirstOrDefault(x => x.Name == request.Name);
            return Task.FromResult(author);
        }
    }
}

The AuthorService inherits from AuthorBase which will be auto-generated from the service definition. Then you override the GetAuthor method provided from the base class. In this method, we search through the in-memory list of authors and return the one that matches the name in the AuthorRequest parameter.

Register the Service

The next step is to register the service in the application configuration in StartUp.cs. Open StartUp.cs, go to Configure method and add the code statement below after line 34 and below the service registration for GreeterService:

endpoints.MapGrpcService<AuthorService>();

Test the Application

At this point, you have all the code necessary to serve the gRPC services and test if it works. However, gRPC services can only go through gRPC clients. For this blog’s demo, you’re going to create a console app that will call the GetAuthor method. However, if you’re building the app on macOS or Windows 7 and below, Kestrel doesn’t support HTTP/2 with TLS on these operating systems. So, you’re going to configure an HTTP/2 endpoint without TLS in Program.cs if you use any of those OS.

Open Program.cs, on line 23 and add the code statement below inside the method call to ConfigureWebHostDefaults

if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
    webBuilder.ConfigureKestrel(options =>
    {
        // Setup a HTTP/2 endpoint without TLS.
        options.ListenLocalhost(5000, o => o.Protocols =
            HttpProtocols.Http2);
    });
  }

Then add the following using statements:

using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Runtime.InteropServices;

What the code you added does is to configure a HTTP/2 endpoint without TLS for Kestrel, if the program is running on macOS.

Your Program.cs file should match the code below:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using System.Runtime.InteropServices;

namespace GrpcAuthor
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        // 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
        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
                     webBuilder.ConfigureKestrel(options =>
                     {
                         // Setup a HTTP/2 endpoint without TLS.
                         options.ListenLocalhost(5000, o => o.Protocols =
                             HttpProtocols.Http2);
                     });
                    }

                    webBuilder.UseStartup<Startup>();
                });
    }
}

Create the gRPC .NET Console Client

Open another instance of Visual Studio and create a new console project named GrpcAuthorClient. You can also add the new project to the previous project’s solution file if you want to. If you use VS Code, then open your command-line application and run dotnet new console -o GrpcAuthorClient, then code -r GrpcAuthorClient to open it in VS Code.

Then install the required NuGet packages. For Visual Studio users, open the Package Manager Console window and run:

Install-Package Grpc.Net.Client
Install-Package Google.Protobuf
Install-Package Grpc.Tools

For VS Code users, open the integrated terminal and run

dotnet add GrpcAuthorClient.csproj package Grpc.Net.Client
dotnet add GrpcAuthorClient.csproj package Google.Protobuf
dotnet add GrpcAuthorClient.csproj package Grpc.Tools

The Grpc.Net.Client package contains the .NET Core client, the Google.Protobuf package contains the protobuf message APIs, and the tooling support for protobuf files is in the Grpc.Tools package.

Create a Protos folder then copy and paste the author.proto file from the server project to it.

Update option csharp_namespace value to GrpcAuthorClient.

Edit the GrpcAuthorClient.csproj file and add an item group with a <Protobuf> element that points to author.proto:

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

Open Program.cs and update the Main() method this code:

static async Task Main(string[] args)
{
    var serverAddress = "https://localhost:5001";
    if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
        // The following statement allows you to call insecure services. To be used only in development environments.
        AppContext.SetSwitch(
            "System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true);
        serverAddress = "http://localhost:5000";
    }

    using var channel = GrpcChannel.ForAddress(serverAddress);
    var client =  new Author.AuthorClient(channel);
    var reply = await client.GetAuthorAsync(new AuthorRequest { Name = "Antonio Gonzalez" });

    Console.WriteLine("Author: " + reply.ToString());

    Console.WriteLine("Press any key to exit...");
    Console.ReadKey();
}

The code creates GrpcChannel with the address of the server. If it’s running on macOS, it configures the app to use insecure HTTP/2 connection and sets the server URL to the insecure HTTP URL. If you’re using .NET 5, you do not need this additional configuration. You only need to ensure you’re using Grpc.Net.Client version 2.32.0 or later.

The GrpcChannel object is then used to instantiate the AuthorClient. The client object is then used to make an asynchronous call using the GetAuthorAsync method. When the server responds, the result is printed to the console.

Add the following using statements to the file.

using System.Net.Http;
using System.Threading.Tasks;
using Grpc.Net.Client;
using System.Runtime.InteropServices;

Now you’re all set to test the service. Start the server project first by running dotnet run in the integrated terminal in VS Code or press Ctrl+F5 to run without the debugger in Visual Studio. When the server is started, run the GrpcAuthorClient client project. You should get the following output:

Author: { "name": "Antonio Gonzalez", "booksAuthored": [ { "title": "Much Ado about nothing" }, { "title": "How to do a split" } ] }

Press any key to exit...

Summary

gRPC is a high-performance RPC framework with pluggable authentication and load balancing features. You define how you want your data to be structured using protocol buffers. You can then use auto-generated source code to easily write and read your structured data to and from a variety of data streams and in a variety of languages.

In this post, you learned how to define a service interface using protocol buffer (version 3) and also implement the service in C#. In the end, you learned how to create a gRPC client and call the methods on the server.

You can find the source code for this post on GitHub.

Additional Resources

  1. Protocol Buffer Language Guide
  2. gRPC docs
  3. Why Microsoft recommends gRPC for WCF developers migrating to .NET Core or .NET 5
  4. How to Add gRPC to Your Blazor App

Peter Mbanugo
About the Author

Peter Mbanugo

Peter Mbanugo is a software developer, tech writer, and maker of Hamoni Sync. When he's not building tech products, he spends his time learning and sharing his knowledge on topics related to GraphQL, Offline-First and recently WebAssembly. He's also a contributor to Hoodie and a member of the Offline-First community. You can follow him on Twitter.

Related Posts

Comments

Comments are disabled in preview mode.