Microservices: Synchronous communication with gRPC 

C# .NET Developer with a passion for web application technologies. I love to learn and experiment with new technologies and approaches in development. I am committed to staying current with industry trends and continuously growing as a developer.

When designing microservices, developers are faced with the challenge of creating a robust, safe, yet efficient way of communicating between microservices. 

In this post, we are going to describe synchronous communication between microservices using gRPC, discuss highlights and challenges, and show a simple example written in .NET 6.

Synchronous communication

Before diving into more details about gRPC and code examples, let’s first describe what synchronous communication is in relation to microservices. 

In synchronous communication, the client sends a request and waits for the response from a called service. The client cannot continue in their task until the response is received. The service usually calls the exposed API of another service via HTTP/HTTPS or gRPC.

This type of communication has its pros and cons: 

Pros: 

  • Popular protocols and support 
  • Real-time communication 

Cons: 

  • Blocking communication (waiting for a response in order to perform the next task) 
  • Autonomy (microservices need to know about each other) 
  • Handling of failures during communication 

What is gRPC? 

gRPC stands for Remote Procedure Call developed by Google. It is an RPC framework focused on transporting binary messages over HTTP/2 protocol very efficiently. It uses Protocol Buffers (Protobuf files), to define the contract (interface) between the backend services. 

Why gRPC?

The gRPC is well suited for use in microservices architectures for several reasons: 

  • Performance (HTTP/3, no JSON serialization/deserialization, binary data) 
  • Streaming (Unary, Client to Server, Server to Client, Bi-directional) 
  • Security (TLS encryption by default) 
  • Strongly typed messages (Protobuf file syntax and native code generation) 
  • Protobuf file support (different programming languages and frameworks) 
  • Multiplatform support 
  • Easier to define contracts than in REST API
  • Easy to debug (Postman and grpcurl even though messages are sent in in non-human readable binary format) 

There is also one potential drawback to using gRPC for microservices. Communication is possible only through HTTP/2, HTTP/3 (Few clients support HTTP v2/v3). 

REST API vs gRPC

But wait a minute, isn’t REST API widely used and more suitable for implementing communication between microservices? REST API has predefined DTOs and endpoints, and we can generate very well-structured Swager API documentation. So, what benefits does gRPC give us in comparison to REST API? 

Rather than defining complex REST APIs, the Protobuf file format simplifies the definition of the data contract between the services. 

Unlike in REST APIs, there is no need to serialize/deserialize the JSON files on each side. Both services generate data contract code based on the .proto file and hence the data types are known at the runtime. 

Communication is more efficient for gRPC compared to REST API. Why is that? It mostly has to do with how data are encoded. Let’s see a simple example of data representation in JSON:

{"id":17,"name":"Coffee"}

The JSON file above takes 25 bytes to represent “id” and “name” values. In comparison, the Protobuf will encode the same data to a message that has only 10 bytes (in this case, ~60% size reduction):

\x08\x11\x12\x06Coffee

If you are more interested in why the gRPC encoded message data has a significantly less size, please see this guide about data encoding and wire types in Protobuf.

What is .proto file?

gRPC uses Protocol Buffers (Protobuf), a language and platform-neutral, extensible mechanism for data serialization. The message data structure and endpoints are defined by the .proto file, which is written based on proto2/proto3 syntax.  

The proto syntax supports a rich set of types such as message types, scalar values, enumerations, and nested types, defines services and remote procedure calls, and much more (see the Proto3 Language Guide). 

gRPC uses protoc to generate native code out of the .proto file. Natively generated code with a default implementation for services and RPC methods can then be overridden with our own implementation of CRUD operations. We will take a more detailed look into the proto file in the following example in .NET 6. 

Example of the gRPC microservice in .NET 6

We are going to show a simple example of gRPC communication between an API gateway and Order/Product microservices. To get product orders, the API gateway has to obtain information about orders and products from Order and Product microservices. In a real-world scenario, there could be a large number of concurrent orders and huge numbers of products, which is why separation into microservices makes sense here. To see the whole implementation, please visit the GitHub repo.

Project architecture

Api, OrderMicroservice, and ProductMicroservice projects are part of the same .NET solution. 

OrderMicroservice and ProductMicroservice projects have: 

  • In-memory database and Entity Framework (code-first approach) with repositories;
  • Proto Services;
  • .proto files to define gRPC contracts; 
  • AutoMapper to map gRPC and Entity DTOs. 

The Api project exposes simple minimal REST API to retrieve the list of product orders composed by calling respective Order and Product microservice gRPC methods. 

Prerequisites

gRPC is very well supported in .NET (since .NET Core 3.0). The implementation has performance advantages compared to other implementations and is multiplatform. We are going to use Grpc.AspNetCore metapackage, which consists of: 

  • Grpc.AspNetCore.Server (gRPC Server library) 
  • Google.Protobuf (Protobuf serialization library) 
  • Grpc.Tools (tools for code generation) 

Product and Order microservice start projects are created based on the standard gRPC template (Visual Studio): 

Program.cs files

Order microservice and Product microservice projects:

For Order microservice and Product microservice projects, we need to add gRPC to the services and http request pipeline:

...
// Services
...
builder.Services.AddGrpc();
...
// HTTP request pipeline
app.MapGrpcService<OrderService>();
...

Api project:

The API project will access Order and Product microservices using Order and Product gRPC clients. Clients have to be added to services:

...
// Services
...
services.AddGrpcClient<OrderServiceProto.OrderServiceProtoClient>(c =>
{
    c.Address = new Uri(
        configuration.GetValue<string>("Microservices:OrderMicroserviceUrl")
    );
});
services.AddGrpcClient<ProductServiceProto.ProductServiceProtoClient>(c =>
{
    c.Address = new Uri(
        configuration.GetValue<string>("Microservices:ProductMicroserviceUrl")
    );
});

Proto files

Let’s see a simple example of the order.proto file that we are going to use in the OrderMicroservice project and briefly explain some of the keywords.

The .proto file starts with a definition of the syntax. In this case we are going to use the latest Proto3 language syntax.

syntax = "proto3";

The csharp_namespace option is used to define the C# namespace hierarchy of where the generated code will be located.

option csharp_namespace = "OrderMicroservice.Protos";

The service keyword of the .proto file defines different RPC methods with input and output messages (types).

service OrderServiceProto {
  ...  
  rpc CreateOrder (CreateOrderRequest) returns (OrderDto);
  rpc AddOrderItem (AddOrderItemRequest) returns (OrderItemDto);
}

The message itself defines either request or response type and contains different fields and their expected order:

message OrderItemDto {
  string id = 1;
  string orderId = 2;
  string productId = 3;  
  int32 quantity = 4;
  float price = 5;
  ...
}

We can use primitive types such as int32, float, string, bool, … as well as nested messages (types):

message AddOrderItemRequest {
  OrderItemDto orderItem = 1;
}

The repeated keyword defines the collection of primitive types or messages:

message OrderDto { 
  string id = 1;
  repeated OrderItemDto orderItems = 2;
}

The stream keyword is used over messages to define that rpc method either takes or returns the stream of messages. It is especially useful in cases where a lot of data needs to be transferred and can be asynchronously read/written from the .NET stream:

service OrderServiceProto {
  rpc GetOrders (GetOrdersRequest) returns (stream OrderDto);
  ...
}

Optional fields? No problem, there is an optional keyword for that as well.

As we can see, the proto3 language has rich syntax to define desired contracts.

The product.proto file for ProductMicroservice project will be defined similarly.

Mews developers enjoy sharing their tech knowledge

Maybe you should join them!

Implementation of gRPC methods defined in .proto file

Based on how the request messages and responses are defined in the .proto file, there are these types of methods: 

  • Unary (Request -> Response) 
  • Client streaming (Request stream -> Response) 
  • Server streaming (Request -> Response stream) 
  • Bi-directional streaming (Request stream -> Response stream) 

The important keyword when implementing gRPC methods is a stream. In Unary methods, we accept the request message and the gRPC call is finished after returning the response. When the stream keyword is used in Client, Server, or Bi-directional gRPC calls in either a request or response, the resulting methods can receive or return multiple messages in the form of the stream, and the gRPC call is finished after all messages are processed. 

We can then override these methods from auto-generated code and implement custom logic for sending and retrieving data.

gRPC methods in Order microservice

The Order microservice will take the role of the gRPC server and will process gRPC order calls from the API gateway gRPC client. For C# project to recognize order.proto file and automatically generate code during build time, the protobuf needs to be included in the OrderMicroservice.csproj file. Also, we need to include Grpc.AspNetCore nuget package:

...
<ItemGroup>
  <Protobuf Include="Protos\order.proto" GrpcServices="Server" />
</ItemGroup>
...
<ItemGroup>
  ...
  <PackageReference Include="Grpc.AspNetCore" Version="2.49.0" />
  ...
</ItemGroup>
...

For the Order microservice we are going to implement gRPC methods such as GetOrders, CreateOrder and AddOrderItem:

public class OrderService : OrderServiceProto.OrderServiceProtoBase
{
    ...
    public override async Task GetOrders(
        GetOrdersRequest request,
        IServerStreamWriter<OrderDto> responseStream,
        ServerCallContext context)
    {
        // Orders logic to obtain data from Db
        ...
        foreach (var orderResult in orderResults)
        {
            if (context.CancellationToken.IsCancellationRequested)
            {
                return;
            }
            await responseStream.WriteAsync(orderResult);
        }
    }
    public override async Task<OrderDto> CreateOrder(
        CreateOrderRequest request,
        ServerCallContext context)
    {
        // Create order item logic
        ...
    }
    public override async Task<OrderItemDto> AddOrderItem(
        AddOrderItemRequest request,
        ServerCallContext context)
    {
        // Add order item logic 
        ...
    }
}

The OrderService overrides methods of OrderServiceProto base class automatically generated based on the order.proto file.

GetOrders is the server streaming method and streams resulting OrderDto messages to the client.

CreateOrder is the unary method and returns the newly added Order as a single OrderDto message.

AddOrderItem is the unary method and returns the newly added OrderItem of the corresponding Order as a single OrderItemDto message.

Similarly, the Product microservice will take the role of the gRPC server and will process gRPC product calls from the API gateway gRPC client. I won’t go into much detail about the implementation as it is pretty much the same as for Order microservice.

Api gateway

Api gateway will assume the role of the gRPC client and will call gRPC methods from both microservices.

Api project will also have the same .proto files that are used in Order and Product microservices, the only difference will be a different namespace:

option csharp_namespace = "Api.Protos";

Both gRPC clients previously registered in services configuration can then be injected into ProductOrderService to fetch orders and products:

public class ProductOrderService : IProductOrderService
{
    ...
    public ProductOrderService(
        ...
        OrderServiceProto.OrderServiceProtoClient orderClient,
        ProductServiceProto.ProductServiceProtoClient productClient)
    {
        ...
    }
    public async Task<IEnumerable<OrderDto>> GetOrdersAsync(
        IEnumerable<string> orderIds)
    {
        try
        {
            _logger.LogInformation("Calling Order microservice method: GetOrders().");
            var ordersRequest = new GetOrdersRequest();
            ordersRequest.OrderIds.AddRange(orderIds);
            var orders = new List<OrderDto>();
            using var ordersCall = _orderClient.GetOrders(ordersRequest);
            await foreach (var order in ordersCall.ResponseStream.ReadAllAsync())
            {
                orders.Add(order);
            }
            return orders;
        }
        catch (Exception e)
        {
            ...
        }
    }
    public async Task<IEnumerable<ProductDto>> GetProductsAsync(
        IEnumerable<string> productIds)
    {
        // Similar logic for fetching products
        ...
    }
}

The minimal API endpoint used to retrieve desired data is going to look like this:

app.MapGet("/order", async (
    IMapper mapper,
    IProductOrderService productOrderService) =>
{
    var orderIds = new[] {
        "f6a95866-a7eb-4be3-90b9-fc81ef23f194",
        "7ae62388-ea9e-4910-a43c-dcf72959dae2"
    };
    var orders = (await productOrderService.GetOrdersAsync(orderIds)).ToList();
    var ordersResult = orders.Select(o => mapper.Map<OrderResult>(o)).ToList();
    var productIds = orders.SelectMany(o => o.OrderItems.Select(oi => oi.ProductId));
    var products = await productOrderService.GetProductsAsync(productIds);
    var productResults = products.Select(p => mapper.Map<ProductResult>(p));
    var productResultsByIds = productResults.ToDictionary(p => p.Id);
    var ordersResultProducts = ordersResult.SelectMany(o =>
        o.OrderItems.Select(oi => oi.ProductInfo)
    );
    foreach (var product in ordersResultProducts)
    {
        var productResult = productResultsByIds[product.Id];
        product.Name = productResult.Name;
        product.Description = productResult.Description;
        product.Size = productResult.Size;
        product.Reviews = productResult.Reviews;
    }
    return new GetOrdersResult
    {
        Orders = ordersResult
    };
}).WithName("GetProductOrders");

For the sake of simplicity, we will obtain orders based on specific order IDs that are predefined, and we will obtain extra information about products referenced in orders by their product IDs.

Then when calling /order REST API endpoint we are going to obtain product orders data composed of gRPC calls from Order and Product microservices:

{
  "orders": [
    {
      "id": "f6a95866-a7eb-4be3-90b9-fc81ef23f194",
      "orderItems": [
        {
          "productInfo": {
            "id": "1bebc704-c7f6-4f77-beed-52ed08ed0716",
            "name": "Cappuccino",
            "description": "A cappuccino is an espresso-based coffee drink that originated in Italy and is prepared with steamed milk foam (microfoam).",
            "size": 2,
            "reviews": [
              {
                "title": "Tastes Great!",
                "description": "Considering its convenience to be able to brew coffee during your busy working hours, this actually tastes great.",
                "startRating": 5
              }
            ]
          },
          "quantity": 1,
          "price": 5,
          "promotionCode": "",
          "extendedGurantee": false
        },
        ...
      ]
    },
    ...
  ]
}

And as we can see in the resulting JSON file, the productId was replaced by productInfo.

Testing gRPC methods with Postman (using .proto file)

We are going to test out gRPC methods by creating gRPC requests in Postman. Postman has a feature to test gRPC calls. You must be part of the Postman workspace to use this feature (signed in with a Postman account):

Sign in with a Postman account and either create or use a default workspace:

Create a new gRPC request with the host set to localhost:7200 and navigate to the Service definition. Import the .proto file for the specific microservice:

After importing the .proto file, select the API (e.g.: Order gRPC):

Then choose your preferred gRPC method to test and let Postman generate an example message for you:

Important: As our API gateway requires a secure connection, we need to enable TLS:

Let’s edit message parameters and invoke the gRPC request:

Normally, the data in gRPC requests and responses are binary encoded, but Postman can recreate the data into a human-readable format (JSON).

Testing gRPC methods with Postman (using gRPC reflection)

The Postman also supports gRPC reflection where there is no need to add .proto contract files and Postman can deduce the contract directly from the .NET microservice application .proto files. The only thing we need to add is the Grpc.AspNetCore.Server.Reflection nuget package and register gRPC reflection into the services:

...
// Services
...
builder.Services.AddGrpcReflection();
...
// HTTP request pipeline
if (app.Environment.IsDevelopment())
{
    app.MapGrpcReflectionService();
}
...

And then, when using gRPC reflection, we just need to specify correct host and activate Server reflection in Service definition of the gRPC request:

As we can see, Postman has a rich set of features to test our gRPC methods. Another great alternative for testing can be the interactive grpculr command-line tool.

Wrapping up

We have learned how to create and test simple synchronous “Client to Server” gRPC communication between an API gateway and two microservices. While there are multiple different approaches for solving communication between microservices such as synchronous HTTP communication (REST API, GraphQL, …) or asynchronous Message/Event-Driven communication (RabbitMQ, Kafka, …), gRPC has its place where efficient, reliable, and safe communication is a goal. As we were able to see, the testing of the gRPC communication in .NET is easy too thanks to the gRPC reflection. And, if you plan to transform your monolithic application into microservices, you should consider gRPC as your starting point.

C# .NET Developer with a passion for web application technologies. I love to learn and experiment with new technologies and approaches in development. I am committed to staying current with industry trends and continuously growing as a developer.
Share: