Working with gRPC in dotnet

Matheus Xavier
.Net Programming
Published in
12 min readJul 22, 2023
dotnet core + gRPC

If you are here it is because you would like to know more about gRPC and probably how we can use it on dotnet, I could try to explain it in a simple way, but with the advent of Artificial Intelligence you could simply ask her what it is and would probably have an even better answer than I can give you.

However, I will try to be a little more practical: The gRPC for dotnet is very interesting for performing communication between services, providing a lightweight, efficient, low latency and high throughput alternative to HTTP APIs.

By the way, this is the answer from GPT Chat:

gRPC (Google Remote Procedure Call) is an open-source high-performance framework developed by Google that enables efficient communication between distributed systems. It is built on top of the HTTP/2 protocol and uses Protocol Buffers (protobuf) as its interface definition language (IDL).

gRPC allows developers to define services and messages using protobuf, which provides a language-agnostic way to define the structure of the data being sent between applications. These service definitions are then used to generate client and server code in multiple programming languages, allowing different applications to communicate with each other seamlessly.

One of the key advantages of gRPC is its efficiency. It uses binary serialization with protobuf, which results in smaller message sizes compared to other formats like JSON or XML. Additionally, gRPC leverages the features of HTTP/2, such as multiplexing, header compression, and server push, to achieve high-performance, low-latency communication.

gRPC supports various programming languages, including C++, Java, Python, Go, Ruby, and many more. It provides bi-directional streaming, allowing both the client and server to send multiple messages over a single connection asynchronously. It also supports various authentication and authorization mechanisms, making it suitable for building secure and scalable microservices architectures.

Overall, gRPC is a powerful framework for building efficient and scalable distributed systems, making it popular for developing services in modern architectures like microservices and cloud-native applications.

The gRPC is faster than HTTP mainly due to four factors:

  1. Protocol: gRPC uses HTTP/2 which performs better than HTTP/1.1
  2. Serialization: gRPC uses Protocol Buffers (protobuf) as its default serialization mechanism
  3. Streaming: gRPC supports bidirectional streaming, allowing both the client and server to send multiple messages over a single connection asynchronous
  4. Code generation: gRPC generates client and server code based on the service definition provided in protobuf

Of these factors, there is one that I think is the most important, which is serialization, usually when we communicate between two services we end up adopting JSON, protobuf compared to it generates a smaller message size because it offers a more compact binary representation, it also offers encoding and decoding more efficient because messages use a predefined schema, which eliminates the need for complex parsing or string manipulation.

After this brief explanation, I will show you a very simple example of how to configure, implement and use gRPC in dotnet.

Hands-on

For our example we have the following scenario: There are two services, a Basket service and a Product service, in the Basket service we have three features:

  1. We can get all products that are in a basket
  2. We can add a new product to the basket
  3. We can remove a product from the basket

The most important functionality is the second one, because when adding a new product to our basket we have to validate that it is a valid product, in addition to knowing its price, description and name, for that we will use gRPC to obtain this information from the Product service.

As I mentioned above, the basket service will get information from the product service, so the Product service will act as the Server and the Basket service as Client for this specific scenario, it is important to mention this because we will have different configurations for the Server and Client.

Feel free to check out the full code directly on github.

Configuring gRPC endpoint on Server

Nuget package

The first thing to do is install the following nuget package:

<PackageReference Include="Grpc.AspNetCore" Version="2.55.0" />

Protobuf

gRPC uses a contract-first approach to API development. Services and messages are defined in .proto files, so after installing nuget package we have to create and define our contract using the proto file, to do that I have created a Proto folder in Product.API and inside it we are going to create the product.proto file:

Proto folder + product.proto file

The product.proto file will be like that:

syntax = "proto3";

package ProductApi;

service ProductGrpc {
rpc GetProduct (GetProductRequest) returns (GetProductResponse);
}

message GetProductRequest {
int32 id = 1;
}

message GetProductResponse {
int32 id = 1;
string name = 2;
string description = 3;
double price = 4;
}

Protocol buffer (protobuf) are used as Interface Definition Language (IDL) by default, the .proto file contains:

  • The definition of the gRPC service
  • The messages sent between clients and services

The product.proto file:

  • Defines the package name, in this case it is ProductApi
  • Defines ProductGrpc service
  • Defines the GetProduct call which receives a GetProductRequest message and returns a GetProductRespose message. In a simpler way, GetProduct works as a method that will receive a GetProductRequest parameter object and returns a GetProductResponse response object.
  • Defines the GetProductRequest, in this case receives only the product id which is an integer. The int type on C# is represented as int32 as protobuf type, check all protobuf scalar data types.
  • Defines the GetProductResponse that contains the product data such as: Id, Name, Description and Price.

After that, our contract is defined, so we need to reference it in csproj getting one of the gRPC benefits which is code generation, where all the code needed to implement our service will be generated based on our proto file, so add the product.proto to <Protobuf> item group:

<ItemGroup>
<Protobuf Include="Proto\product.proto" GrpcServices="Server" />
</ItemGroup>

By default, a <Prodobuf> reference generates a concrete client and service base class. The reference element’s GrpcServices attribute can be used to limit C# asset generation. Valid GrpcServices options are:

  • Both (default when not present). Generates Server and Client code.
  • Server. Generate only the Server side code.
  • Client. Generates only the Client side code.

In our sample, as I said before the Product Service will act as a service for the Basket Service so it will use the Server option.

Handling gRPC call

After this setup an abstract ProductGrpBase type was generated containing a virtual GetProduct method, now we can override this method implementing the logic to handle the gRPC call, so I created a new folder in the project called Grpc that will have all the Grpc services and inside it I have created a new ProductService class:

Grpc folder + ProductService.cs

The ProductService.cs will be like that:

using Grpc.Core;

using Microsoft.EntityFrameworkCore;

using Product.API.Infrastructure;
using Product.API.Model;

using ProductApi;

namespace Product.API.Services;

public class ProductService : ProductGrpc.ProductGrpcBase
{
private readonly ProductContext _context;

public ProductService(ProductContext context)
{
_context = context;
}

public override async Task<GetProductResponse> GetProduct(GetProductRequest request, ServerCallContext context)
{
ProductItem? product = await _context.Products
.Where(p => p.Id == request.Id)
.AsNoTracking()
.FirstOrDefaultAsync();

if (product is null)
{
throw new RpcException(new Status(StatusCode.NotFound, $"Could not found product with id: {request.Id}"));
}

if (!product.Active)
{
throw new RpcException(new Status(StatusCode.NotFound, $"Product with id: {request.Id} is disabled"));
}

return new GetProductResponse
{
Id = product.Id,
Name = product.Name,
Description = product.Description,
Price = product.Price,
};
}
}

The implementation itself is very simple, we overridden the method, so we are already receiving the parameter GetProductRequest and the method already expects returning a GetProductResponse, so we search a product with the Id that was received via parameter, if we do not find it we will throw an RpcException indicating that it was not possible to find a product with that Id, if the product exits but it is inactive then we will also throw an RpcException indicating that the product exists but that it is disabled, finally, if the product was found and it is active then we will return the product data through the GetProductResponse generated type.

After this brief explanation, I think there are two points worth commenting on:

  1. How does the Status work on gRPC calls?
  2. Why are we throwing an RpcException?

gRPC Status Codes

HTTP requests have a response status, these statuses help us interpret the request result, for example:

  • 200 means it was OK
  • 400 means there was some invalid data — bad request
  • 500 means an internal error occurred
  • and so on…

gRPC calls also have the statuses, but the list is a little bit different, for example:

  • 0 means it was OK
  • 3 is InvalidArgument which would be equivalent to BadRequest
  • 5 is NotFound
  • Check the complete list.

In the example above we are working with the OK status code which is the default status and the NotFound which we are using when the product does not exist on the database or if the product is disabled.

RpcException

When viewing the code, if you’ve never had contact with the gRPC implementation, perhaps the first thing that comes to your mind is: Why are we throwing an exception when we can’t find the product? This is because exceptions usually end up being associated with something negative. In our scenario we could have another approach, instead of throwing an exception we could simply change the request status, it would look like this:

if (product is null)
{
context.Status = new Status(StatusCode.NotFound, $"Could not found product with id: {request.Id}");
return new GetProductResponse();
}

if (!product.Active)
{
context.Status = new Status(StatusCode.NotFound, $"Product with id: {request.Id} is disabled");
return new GetProductResponse();
}

When overriding the virtual method we receive a second parameter that is the request context, this context has a status code that by default it is OK, we can simply change this status to NotFound, but even following this approach, gRPC will continue throwing an exception to the client, basically every request that does not have the OK status code will make gRPC to throw this exception, so I think it’s more interesting to make this explicit in the code by throwing this exception by myself, but at the end the result it will be the same.

In the example I presented, the exception is just sending a status code and a brief message, but if necessary it is also possible to add a metadata where we can send supporting data to our client and a longer exception message, below is an example:

if (product is null)
{
throw new RpcException(
status: new Status(StatusCode.NotFound, $"Could not found product with id: {request.Id}"),
trailers: new Metadata
{
{ "pId", request.Id.ToString() },
{ "x1", "Aditional data" }
},
message: "invalidproductid");
}

if (!product.Active)
{
throw new RpcException(
status: new Status(StatusCode.NotFound, $"Product with id: {request.Id} is disabled"),
trailers: new Metadata
{
{ "pId", request.Id.ToString() },
{ "x1", "Aditional data" }
},
message: "productdisabled");
}

Now that I’ve explained a bit more about status codes and exceptions, let’s move on to our client implementation.

Configuring gRPC on Client

Nuget packages

At the client side we will need following nuget package:

<PackageReference Include="Grpc.AspNetCore" Version="2.54.0" />
<PackageReference Include="Grpc.AspNetCore.Server.ClientFactory" Version="2.54.0" />
<PackageReference Include="Grpc.Tools" Version="2.56.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

Protobuf

The first thing we did on the Server side was to define our contract by creating the proto file, here on the Client side it will be the same thing, however it would not be necessary to recreate the file, we will simply refer to the already existing file, so we will add the following code to our csproj:

<ItemGroup>
<Protobuf Include="..\..\Product\Product.API\Proto\product.proto" GrpcServices="Client" />
</ItemGroup>

Register gRPC client

Now the GrpcServices element is using the Client option because we are only going to consume the endpoint, so we only need it to generate the necessary code for the Client. After that, we will have our concrete class for the client, so we can refer to ProductGrpc.ProductGrpcClient which is the generated code that will be responsible for consuming our endpoint, lets register the gRPC client, this step is very simple, we can use the generic AddGrpcClient extension method inside our Program class, the only thing that will be necessary is to indicate in which address our server is running:

Product API address

If you check our server’s lauchSettings.json file (Product.API), you’ll see that it’s running at https://localhost:7087, so that’s the Uri we’re going to use.

builder.Services
.AddGrpcClient<ProductGrpc.ProductGrpcClient>((services, options) =>
{
options.Address = new Uri("https://localhost:7087");
});

The gRPC client type is registered as transient with dependency injection (DI). The ProductGrpc.ProductGrpcClient can now be injected and consumed directly, so we are ready to write the code that will be responsible for consuming this endpoint, we are going to add a Services folder to the project and create a ProductService inside it:

Services folder + ProductService.cs

The ProductService will be responsible for making the gRPC call to the Product API and handling the response, at first it’s a very simple code:

using Basket.API.Model;

using Grpc.Core;

using ProductApi;

namespace Basket.API.Services;

public class ProductService : IProductService
{
private readonly ProductGrpc.ProductGrpcClient _client;
private readonly ILogger<ProductService> _logger;

public ProductService(
ProductGrpc.ProductGrpcClient client,
ILogger<ProductService> logger)
{
_client = client;
_logger = logger;
}

public async Task<ProductItem?> GetProductAsync(int productId)
{
GetProductRequest request = new() { Id = productId };

try
{
GetProductResponse response = await _client.GetProductAsync(request);

return new ProductItem(
response.Id,
response.Name,
response.Description,
response.Price);
}
catch (RpcException e)
{
_logger.LogWarning(e, "ERROR - Parameters: {@parameters}", request);

return null;
}
}
}

As I mentioned before, after registering our gRPC client it was possible to consume it through the constructor, we then created the GetProductAsync method that will receive the product id, fill in the GetProductRequest and make the call to our service, then it will receive the GetProductResponse and map it to our ProductItem. If the product id does not exist or if it is deactivated, an RpcException will be thrown, so we are handling this scenario through try/catch, if this occurs we will log a warning message and return a null value.

Controller

Now we can finally finish our functionality of adding a product to our basket, the code for this endpoint looks like this:

public record BasketItemData(int ProductId, int Quantity);


[HttpPost("api/v1/customers/{id:guid}/baskets")]
public async Task<IActionResult> AddBasketItem(Guid id, BasketItemData basketItem)
{
if (basketItem.Quantity <= 0)
{
return BadRequest("Invalid quantity");
}

CustomerBasket? basket = await _repository.GetBasketAsync(id) ??
new CustomerBasket(id, new List<BasketItem>());

bool productAlreadyInBasket = basket.Items.Any(p => p.Id == basketItem.ProductId);

if (productAlreadyInBasket)
{
basket.Items.First(p => p.Id == basketItem.ProductId).IncreaseQuantity(basketItem.Quantity);
}
else
{
ProductItem? product = await _productService.GetProductAsync(basketItem.ProductId);

if (product is null)
{
return BadRequest("Product doest not exist");
}

basket.Items.Add(new BasketItem(
product.Id,
product.Name,
product.Description,
product.Price,
basketItem.Quantity));
}

await _repository.UpdateBasketAsync(basket);

return Ok();
}

First we validate the quantity of the product, if it is negative we return a BadRequest, after that we check if there is already a basket with the informed id, if not, we will create a new one, then we check if the product that is being added is already in the basket, if yes, we simply increase the quantity, if not, it means that it is a new product for that basket and then we will use our service to verify if it is a valid product through the gRPC call to Product.API, if the returned value is null, it means that the product does not exist or that it is deactivated, in either case we are returning a BadRequest because it is not a valid product id.

Conclusion

gRPC is not something new, however it is not so commented, but it is something very interesting and a very viable alternative to HTTP, while I was writing this article, a news came out saying that LinkedIn started using protobufs and that this reduced latency by up to 60%, I think that currently few people have adopted gRPC on dotnet, so I wrote this article to demystify the subject a little bit and give a very easy and practical example, I hope you liked it :D

Github project with full code sample.

Thank you for reading and feel free to contact me if you have any questions.

--

--