Post Cover

Building a simple Client-Server Interaction with gRPC in ASP.NET Core

ASP.NET Core gRPC Posted May 30, 2021

A Remote Procedure Call (RPC) is a facet of distributed systems where clients call procedures (or methods) inside the server as if they were methods in another class. The client doesn't know the underlying details about how the call is being made to the remote procedure.

gRPC is an open source remote procedure call system initially developed at Google as the next generation of RPC.

In this article, let's talk in brief about how gRPC works and how we can implement a simple Client-Server interaction with gRPC in ASP.NET Core (.NET 5) stack.

gRPC system uses a contract mechanism - the client defines a contract with details about the procedure it knows and how it expects to communicate (parameters and return).

The server takes this contract and implements the Services based on this contract. The client calls these procedures that it already defined in its contract on the server as if its yet another method call, using a channel object.

The entire interaction happens between client-server over HTTP/2 with Protobuf as the underlying data encoding mechanism. For comparison, a REST API written over HTTP generally uses JSON as the data format.

Protobuf requires strict specification of the message types and procedure definition, and provides a common experience irrespective of the platform it is being implemented.

Long story short, building a gRPC system requires the following steps:

  1. Definition of a procedure contract - written in a proto file
  2. A Server that digests this contract and builds a Service implementation
  3. A Client that digests this contract and builds a Client proxy (component)
  4. Testing the interaction by calling the Server method on the Client via its local proxy

gRPC and ASP.NET Core (.NET 5):

For our experiment, let's build a WeatherService that returns Weather information for a given Location. I'm interested in building this interaction over a gRPC service so let's build a WeatherService and a WeatherClient that calls the method from this service.

Let's get started by creating a new ASP.NET Core 5 project.

> dotnet new web --name WeatherGrpcService

It creates an empty project with no packages pre-installed. Alternatively we do have a grpc template available which comes with a proto file and a Service implementation. But starting from an empty project make us grasp things a bit easier.

Let's create two folders in the project - protos and Services. protos folder holds the proto files and the Services folder holds the C# implementation of the Protobuf contracts.

Defining the contract - proto file:

Inside the protos folder, create a new file "weather.proto" which contains the proto3 contract for the Weather service we're interested in building. Conceptually the file holds contract for a single method GetUpdate which returns WeatherInfo data for a given Location object containing Latitude and Longitude.

syntax = "proto3";

package weather;

option csharp_namespace = "WeatherGrpcService";

service Weather {
    rpc GetUpdate (Location) returns (WeatherInfo);
}

message Location {
    double Latitude = 1;
    double Longitude = 2;
}

message WeatherInfo {
    double Temperature = 1;
    double Precipitation = 2;
    double Humidity = 3;
    bool IsRainfallExpected = 4;    
    string Prediction = 5;
}

Next, add this proto file in the csproj file as a Protobuf definition.

<ItemGroup>
    <Protobuf Include="protos/weather.proto" GrpcServices="Server" />
</ItemGroup>

Installing Packages and Implementing the Service:

We're now done with the initial part. Next, we need to install the necessary packages for building the gRPC server.

dotnet add package Grpc.AspNetCore

Once this package is installed, we're now ready to create our GrpcService that uses this contract that we created earlier. Inside the Services folder, create a new file WeatherService.cs which contains a class WeatherService.

namespace WeatherGrpcService.Services
{
    public class WeatherService : WeatherGrpcService.Weather.WeatherBase
    {

    }
}

Observe the base class that the WeatherService is extending. The WeatherBase is a class generated from the proto file that we added earlier, which is under the namespace HelloGrpc.Weather - a combination of the csharp_namespace and the package we specified.

We can now override the GetUpdate() method that is available under the WeatherBase and add our implementation. The class now looks like below:

using System.Threading.Tasks;
using Grpc.Core;

namespace WeatherGrpcService.Services
{
    public class WeatherService : WeatherGrpcService.Weather.WeatherBase
    {
        public override Task<WeatherInfo> GetUpdate(Location request, 
            ServerCallContext context)
        {
            return Task.FromResult(new WeatherInfo
            {
                Humidity = 34.0,
                Precipitation = 4,
                Temperature = 34,
                IsRainfallExpected = false,
                Prediction = $"Weather Update for 
                {request.Latitude}, {request.Longitude} 
                    => Its going to be a sunny day."
            });
        }
    }
}

The ServerCallContext is similar to the HttpContext, but in a gRPC perspective. It contains additional data that the client sends while calling the Remote method. Other parameters are similar to what we defined in the proto file.

As a last step, we register this gRPC service inside the Startup class under the Routing middleware.

// ConfigureServices method
services.AddGrpc();

// Configure method
app.UseEndpoints(endpoints =>
{
    endpoints.MapGrpcService<WeatherService>();
});

Testing the Interaction - Building the Client

To test this GrpcService, we need a GrpcClient that can call this method. To do this, let's just create a simple console application that consumes this proto contract.

> dotnet new console --name GrpcClient

On the client project, we need to add the required tooling to be able to work with GrpcServices. For that we install the below packages:

> dotnet add package Google.Protobuf 
> dotnet add package Grpc.Net.Client
> dotnet add package Grpc.Tools

Once installed, we're now ready to consume the WeatherGrpcService. But the client needs to know the template of the GrpcService first to be able to create its own client implementation.

For that we'd add the same proto file that we created on the WeatherGrpcService into the client as well.

Let the file be under protos folder similar to the Server. Also we need to add this contract in the csproj file of the client.

The file (on the client side) now looks like this:

syntax = "proto3";

package weather;

option csharp_namespace = "GrpcClient";

service Weather {
    rpc GetUpdate (Location) returns (WeatherInfo);
}

message Location {
    double Latitude = 1;
    double Longitude = 2;
}

message WeatherInfo {
    double Temperature = 1;
    double Precipitation = 2;
    double Humidity = 3;
    bool IsRainfallExpected = 4;    
    string Prediction = 5;
}

We just adjusted the csharp_namespace to suit the client namespaces. Finally we add this proto in the csproj as below:

<ItemGroup>
    <Protobuf Include="protos\weather.proto" GrpcServices="Client" />
</ItemGroup>

Observe that the GrpcServices value is now "Client" instead of the "Server" we gave before. Once we're done till here, we build the project to let dotnetcore create the Client files for us.

Now for the client invocation: we do it in three simple steps:

  1. Create a channel
  2. Create a client and pass the channel
  3. Call the method

We do all these inside the Main method. The code looks like below:

namespace GrpcClient
{
    class Program
    {
        static async Task Main(string[] args)
        {
            // create channel
            var channel = GrpcChannel.ForAddress("https://localhost:5001");

            // create client and pass the channel
            var client = new GrpcClient.Weather.WeatherClient(channel);

            // call the method from the client
            var weatherInfo = await client.GetUpdateAsync(new Location
            {
                Latitude = 14.10,
                Longitude = 77.00
            });

            // I'm lazy to write print statements for every property
            // so I just loop through all the properties inside the WeatherInfo
            foreach (PropertyInfo prop in weatherInfo.GetType().GetProperties())
            {
                var name = prop.Name;
                var value = prop.GetValue(weatherInfo, null);
                Console.WriteLine($"{name}=>{value}");
            }

            Console.ReadLine();
        }
    }
}

Now to test this, we first run the GrpcService (server) which we assume to be running on https://localhost:5001. That's the address we added in the ForAddress() method. Next, we run this console application which makes an RPC call to the server and invokes the method GetUpdate() - in this case we're invoking the async version which is GetUpdateAsync() - the auto-generated client provides both the versions.

The output in the client looks like this:

Parser=>Google.Protobuf.MessageParser`1[GrpcClient.WeatherInfo]
Descriptor=>Google.Protobuf.Reflection.MessageDescriptor
Temperature=>34
Precipitation=>4
Humidity=>34
IsRainfallExpected=>False
Prediction=>Weather Update for 14.1, 77 => Its going to be a sunny day.

Conclusion - Final Thoughts:

gRPC is a next-generation RPC stack created for making client-server messaging systems easy. It works and supports only Protobuf as the medium so it eliminates any discussion for other encoding types. It also uses HTTP/2 as the Protobuf making it secure, but not supported on browsers so it needs a client proxy to test.

In the next article, let's look at how we can secure these client-server interactions so that the server can ignore unwanted RPC calls from clients it doesn't recognize.

We shall also look at how we can work with data streams with gRPC - where for a given request the server responds to client with a stream of data and how the client can handle it.

Author-Image

Ram

I'm a full-stack developer and a software enthusiast who likes to play around with cloud and tech stack out of curiosity.

Hope this article was helpful. You can now show us your support. 😊

We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept