BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Getting Started with gRPC and .NET

Getting Started with gRPC and .NET

Key Takeaways

  • .NET Core supports gRPC development out of the box, with both client and server support
  • gRPC relies on HTTP2 
  • REST and gRPC don't cancel each other: each has their specific use cases 
  • gRPC is built with Interoperability in mind to update components without breaking any current implementation
  • gRPC supports both single- and bi-directional streaming

 

An API, in essence, is an agreement between the server and the client that specifies how the server will provide specific data based on the client's request.

When building APIs, different technologies come to mind. Depending on our needs, the technology we choose to develop an API will change. In this day and age, there are two leading technologies used primarily for creating APIs:

  • gRPC
  • REST

Both technologies utilize HTTP as the transport mechanism. Although they share the same underlying transport mechanism, their implementations are entirely different.

Let’s compare these two technologies and then delve deep into gRPC.

REST

REST is a set of architectural constraints, not a protocol or a standard. API developers can implement REST in a variety of ways.

In order for an API to be considered as RESTful, there are some constraints to which we need to adhere:

  • Client-Server Architecture: all requests must utilize HTTP as the transport mechanism;
  • Stateless: The API should be stateless, meaning the server should not store any state about the client session on the server side. Each request from the client to the server must contain all of the necessary information to understand the request. The server cannot take advantage of any stored context on the server.
  • Cacheable: all data that streamlines client-server interactions must be cacheable, meaning they can be stored to be retrieved and used later.
  • Uniform interface: there must be an interface between client and server so that information is transferred in a standard form.
  • Layered system: the servers between the client’s request and the server’s response must be organized according to their responsibilities, in a manner that they don’t affect the request or the response.

gRPC

Built on the solid foundation of the RPC (Remote Procedure Call) protocol, gRPC comes into the API world. It is a free and open source framework developed by Google that utilizes HTTP/2 for API communication, hiding the HTTP implementation from the API designer.

gRPC has many characteristics that make it a foundation block for the next generation of web applications, from microservices to Web/mobile API communication:

  • Interoperability: regardless of the current HTTP version, the protocol must be able to adapt and change no matter how the infrastructure changes (upgrading from HTTP 2 to HTTP 3 for example).
  • Layered architecture: key facets of the stack must be able to evolve and be upgraded independently without the need to disrupt any application that uses it.
  • Payload Agnostic: different services can require different message types and encodings such as Protobuf (Protocol buffer), Json, XML, and many others. gRPC allows for all that and also for payload compression by utilizing pluggable compression mechanisms .
  • Streaming: gRPC allows the relay of  large data sets from server to client and vice versa. 
  • Pluggable: gRPC is built to plug different functionalities and services that are required to meet our demands, such as health checks, fail-overs and load balancing. The framework implementation provides extension points which allows for plugging in these functionalities.

gRPC is part of the cloud native computing foundation (CNCF), similarly to docker and kubernetes.

In a nutshell, the benefits of gRPC are:

  • modern, fast
  • open source
  • utilise HTTP/2
  • Language neutral
  • Easy to add authentication, logging.

In order to use gRPC:

  • We need to define messages and services using Protobuf (Protocol Buffer)
  • The gRPC code will be generated for us and will need to provide an implementation
  • One .proto file is supported by 12 different languages for both server and client

By default, gRPC uses Protocol Buffers, Google's open source mechanism for serializing structured data:

  • Its language agnostic
  • The code can be generated for any modern language
  • The transmitted data is binary and efficient
  • Highly scalable
  • Allows us to send large sum of data
  • Allows us to expand and evolve our API

Case Study:

A modern approach to today's technology trend is to build microservices. In this case, let us study the process of building an airline ticketing system: 

The picture above represents an airline ticketing system based on microservices. There are a few key points related to this type of architecture that we should observe:

  • Microservices are often built in different languages. Being that so, the booking management service could be built on .NET, while the payment processing could be in Java, and the Passenger information could use NodeJs.
  • Each service has a different business functionality

Let us assume that we have microservices written in different languages trying to talk to each other. When these microservices want to exchange information they need to agree on a lot of things, such as:

  • API to exchange data
  • Data format
  • The Error format
  • Rate limitation

REST is one of the most popular choices to build APIs. However, this decision depends on a lot of architectural considerations related to our implementation:

  • Designing the data model type;
  • How the endpoints will look like;
  • How errors are being handled;
  • How many calls a single client can make;
  • How will authorization work;

With these considerations in mind, let’s take a look at the differences between gRPC and REST:

gRPC

  • Contract-first approach to API development: contracts (services and messages) are defined in *.proto files, which are the heart of gRPC. It’s a language-neutral way to define APIs. These files are then used by other programming languages for code generation (e.g. strongly-typed client and message classes).
  • Content is binary: HTTP/2 and Protobuf are binary protocols and the content is designed for computers and high performance.
  • gRPC is designed to hide the complexity of remoting. By using the gRPC library and the associated code generation, we don't need to worry about routing, headers, or serialization. When it comes to calling a method on a client, all we need to do is invoke that method.
  • gRPC supports bidirectional asynchronous streaming: a single gRPC call establishes a stream in which both the client and the server can send a stream of messages to each other, at any time. Server and Client streaming (where either the response or the request is a stream) are also supported.
  • gRPC is designed for both high-performance and high-productivity design of distributed applications.

REST API

  • ContentFirst approach to API development (URL, HTTP method, Json): focus on readability and formatting.
  • Content is text-based (HTTP 1.1 and Json), thus being human readable. As a consequence, they are great for debugging, but not for performance.
  • Emphasizes on HTTP a lot more. We need to consider the low-level concerns, which is good since we get a lot of control over HTTP requests.
  • CRUD-oriented.
  • Widest audience: every computer is able to use HTTP/1.1 and Json. Easy to get started.

Based on this comparison, we can see that both have their pros. However, we can see that gRPC provides a powerful set of features for microservices-based scenarios.

Creating a Server-Client application with gRPC

Before start coding, we will need the following software to be installed on our machine:

Once we have the software above installed, we need to create our project structure (in this article, we will use the dotnet commands directly from the terminal/command line):

dotnet new grpc -n GrpcService

We also need to configure the SSL trust:

dotnet dev-certs https --trust

Let’s open our new project in VS Code and see what was created. We can see that out of the box we get the following:

  • Protos folder
  • Services folder

Inside our Protos folder, we have a greet.proto file. As we mentioned before, the .proto file is the language-neutral way to define our APIs.

We can see from the file we have a Greeter  service and a SayHello method. Think about the Greeter service as a controller and the SayHello method as an action. The contents of the .proto file are shown below:

// Specify the latest schema that we can use
syntax = "proto3";

// define the name space for this proto usually its the same as
// our Grpc Server
option csharp_namespace = "GrpcService";

package greet;

// we can think of a service as a class
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply);
}

// The request message is like a model in c#
// defining a property inside of it 
// the number is for the ordering of the property 
message HelloRequest {
  string name = 1;
}

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

The SayHello method takes a HelloRequest (which is a message) and returns a HelloReply (which is also a message).

Inside our GreeterService file, we can see we have a GreeterService class that inherits Greeter.GreeterBase, which is auto-generated code from the .proto file.

 In the SayHello method, we are taking a request (HelloRequest) and returning a response (HelloReply). These classes are also auto-generated for us from the .proto file.

Basically, code generation is responsible for generating the files for us based on the .proto file definition. gRPC is doing all of the heavy-lifting in generating, routing, and serializing the code for us. All we need to do is implement the base class and override the implementations of the method.

Let’s try running our gRPC service:

dotnet run

We can see from the result in the auto-generated endpoint that we cannot use gRPC as we use the REST client from a web browser. In this case, we need to create a gRPC client to be able to communicate with the service. gRPC also requires the .proto file for our client, since it’s a contract-First RPC framework. Currently our web browser doesn't know anything about the client (we don’t have a .proto file), so it doesn't know how to process requests.

Let’s create our custom .proto file called customers.proto. This file must be created inside our Protos folder, with the following content:

syntax = "proto3";

option csharp_namespace = "GrpcService";

package customers;

service Customer {
    rpc GetCustomerInfo (CustomerFindModel) returns (CustomerDataModel);
}

message CustomerFindModel {
    int32 userId = 1; // bool, int32, float, double, string
}

message CustomerDataModel {
    string firstName = 1;
    string lastName = 2;
}

Once you save the file above, we need to add it to the .csproj file:

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

Now we need to build our application:

dotnet build

The next step is to add our CustomerService class inside our Services folder and update its content as follows:

public class CustomerService : Customer.CustomerBase
{
    private readonly ILogger<CustomerService> _logger;
    public CustomerService(ILogger<CustomerService> logger)
    {
        _logger = logger;
    }

    public override Task<CustomerDataModel> GetCustomerInfo(CustomerFindModel request, ServerCallContext context)
    {
       CustomerDataModel result = new CustomerDataModel();

        // This is a sample code for demo
        // in real life scenarios this information should be fetched from the database
        // no data should be hardcoded in the application
       if(request.UserId == 1) {
           result.FirstName = "Mohamad";
           result.LastName = "Lawand";
       } else if(request.UserId == 2) {
           result.FirstName = "Richard";
           result.LastName = "Feynman";
       } else if(request.UserId == 3) {
           result.FirstName = "Bruce";
           result.LastName = "Wayne";
       } else {
           result.FirstName = "James";
           result.LastName = "Bond";
       }

        return Task.FromResult(result);
    }
}

Now we need to update our Startup.cs class to inform our application that we have a new endpoint for the new service that we have created. In order to do that, inside our Configure method (in app.UserEndpoints) we need to add the code below: 

endpoints.MapGrpcService<CustomerService>();

MacOS note:

Since MacOS doesn't support HTTP/2 over TLS, we need to update the Program.cs file with the following workaround:

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

The next step is to create our Client application:

dotnet new console -o GrpcGreeterClient

Now we need to add the necessary packages to the client console application for it to be able to recognise gRPC. This can be done in the  GrpcGreeterClient class:

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

Since we need the client to have the same contracts as the server, we need to add the .proto files we created in the previous step to our client application. In order to do that:

1- First, we need to add a folder to the client project called Protos;

2- We need to copy the content of the Protos folder from the gRPC greeter service to the gRPC client project

  • greet.proto
  • customers.proto

3- After pasting the file we need to update the namespace for it to be the same as our client application:

option csharp_namespace = "GrpcGreeterClient";

4- We need to update the GrpcGreeterClient.csproj file so it’s aware of the new .proto files we added:

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

This Protobuf element is how the code auto-generation feature knows about the .proto files. With the alterations above, we are saying here we want our client to use the new .proto files we added.

We need to build our client and make sure everything is building successfully:

dotnet run

Now let's add some code to our console application to call our server. We need to do the following alterations inside our Program.cs file:

// We create a channel that represents the connection from client to the server
// The URL that we add is provided from Kestrel in the server
var channel = GrcpChannel.ForAddress("<https://localhost:5001>");

// this the strongly typed client that was create for us from code generation
// when we added the .proto file
var client = new Greeter.GreeterClient(channel);

var response = await client.SayHelloAsync(new HelloRequest
{
	Name = "Mohamad"
});

Console.WriteLine("From Server: "  + response.Message);

var customerClient = new Customer.CustomerClient(channel);

var result = await customerClient.GetCustomerInfoAsync(new CustomerFindModel()
{
    UserId = 1
});

Console.WriteLine($"First Name: {result.FirstName} - Last Name: {result.LastName}");

Now let's add stream capabilities to our application.

Let's go back to our customers.proto and add a stream method inside out Customer service:

// since we are going to return a list of customers
// we cannot return lists in gRPC - so we need to return a stream
rpc GetAllCustomers (AllCustomerModel) returns (stream CustomerDataModel);

As you can see, we have added the keyword stream in the return, which means we are adding a stream of "multiple" replies.

as well we need to and an empty message

// in gRPC we cannot have a method with empty parameters
// so we create an empty message 
message AllCustomerModel {

}

To implement this method we need to go to the Services folder and add the following code to our CustomerService class::

public override async Task GetAllCustomers(AllCustomerModel request, IServerStreamWriter<CustomerDataModel> responseStream, ServerCallContext context)
{
    var allCustomers = new List<CustomerDataModel>();

    var c1 = new CustomerDataModel();
    c1.Name = "Mohamad Lawand";
    c1.Email = "mohamad@mail.com";
    allCustomers.Add(c1);

    var c2 = new CustomerDataModel();
    c2.Name = "Richard Feynman";
    c2.Email = "richard@physics.com";
    allCustomers.Add(c2);

    var c3 = new CustomerDataModel();
    c3.Name = "Bruce Wayne";
    c3.Email = "bruce@gotham.com";
    allCustomers.Add(c3);

    var c4 = new CustomerDataModel();
    c4.Name = "James Bond";
    c4.Email = "007@outlook.com";
    allCustomers.Add(c4);

    foreach(var item in allCustomers)
    {
        await responseStream.WriteAsync(item);
    }
}

Now we need to copy the changes from the server’s customers.proto file to the client’s customers.proto file: 

service Customer {
    rpc GetCustomerInfo (CustomerFindModel) returns (CustomerDataModel);

    // since we are going to return a list of customers
    // we cannot return lists in gRPC - so we need to return a stream
    rpc GetAllCustomers (AllCustomerModel) returns (stream CustomerDataModel);
}

// in gRPC we cannot have a method with empty parameters
// so we create an empty message 
message AllCustomerModel {

}

Now we need to build the application again:

dotnet build

The next step is updating the Program.cs file in the GrpcClientApp to process the new stream method:

var customerCall = customerClient.GetAllCustomers(new AllCustomerModel());

  await foreach(var customer in customerCall.ResponseStream.ReadAllAsync())
  {
      Console.WriteLine($"{customer.Name} {customer.Email}");
  }

Now let’s go back to our GrpcGreeter and update our greet.proto file with a stream method:

rpc SayHelloStream(HelloRequest) returns (stream HelloReply);

As you can see, we have added the keyword stream in the return, which means we are adding a stream of "multiple" replies. To implement this method, we need to go to the Services folder and in our GreeterService we need to add the following:

public override async Task SayHelloStream(HelloRequest request, IServerStreamWriter<HelloReply> responseStream, ServerCallContext context)
{
	for (int i = 0; i < 10; i ++)
	{
		await responseStream.WriteAsync(new HelloReply 
		{
			Message = "Hello " + request.Name + " " + i
		});

		await Task.Delay(TimeSpan.FromSeconds(1));
	}
}

Now we need to copy the changes in the greet.proto file from the server to the client and build it. Let’s add the line below in our client application greet.proto file:

rpc SayHelloStream(HelloRequest) returns (stream HelloReply);

Make sure you build the application after saving the .proto file.

dotnet build

Now let's open the Program.cs and utilize the new method:

var call = client.SayHelloStream(new HelloRequest
{
    Name = "Mohamad"
});

await foreach(var item in call.ResponseStream.ReadAllAsync())
{
    Console.WriteLine("Result " + item.Message);
}

This is an example of how we can implement a gRPC client-server application in .NET 5.

In Summary

We can see the power of gRPC in building our applications, but this power doesn’t come easy as building gRPC services requires much more setup time and coordination between the client and the server. On the other hand, with REST we can get started directly by consuming endpoints almost without any setup.

gRPC will not necessarily replace REST, since both technologies have their specific applications.Choosing the right technology for your project will be entirely based on the business scenarios and the requirements. 

About the Author

Mohamad Lawand is a determined and forward-thinking Technical Architect with 13+ years of experience, working across a wide range of industries, from financial institutes to government entities. Proactive and adaptable, specializing in SaaS and Blockchain technologies across multiple platforms. Mohamad also has a Youtube Channel where he shares his knowledge.


 

Rate this Article

Adoption
Style

BT