First experience with gRPC
Tags: grpc, dotnet, csharp, learning
Published: 2024-06-21 14:30:34

I heard some colleagues talking about gRPC framework and its benefits and advantages regarding Rest, so I dig a little in the gRPC world and its concepts to learn new ways to develop an API.

### What is gRPC?
gRPC is an open-source API architecture and system. It’s based on the Remote Procedure Call (RPC) model. While the RPC model is broad, gRPC is a specific implementation.
gRPC is a system that implements traditional RPC with several optimizations. For instance, gRPC uses Protocol Buffers and HTTP 2 for data transmission. It also abstracts the data exchange mechanism from the developer.

### gRPC vs REST
The following image compares the basics of REST APIs and gRPC:

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2egdqgsezavydvnc7dx2.png)


## Implementation

To better learn the gRPC concepts, I created a simple chat room scenario.

#### Project Setup
We'll create two projects within a solution:

- ChatAppServer: The gRPC server that handles chat room logic.
- ChatAppClient: The client application that users will run to join chat rooms and send messages (This is a console App project).

Then we'll install the following nuget packages:

- Grpc.AspNetCore
- Grpc.Tools
- Grpc.AspNetCore.Server.ClientFactory (this one only on the ChatAppClient project)

#### Proto file
We'll need to create a '.proto' file to define the gRPC service. This file specifies the service methods and message types.
This is my chat.proto file:

```
syntax = "proto3";

option csharp_namespace = "ChatApp";

package chat;

service ChatService {
  rpc JoinRoom (JoinRoomRequest) returns (JoinRoomResponse);
  rpc SendMessage (SendMessageRequest) returns (SendMessageResponse);
  rpc ReceiveMessages (ReceiveMessagesRequest) returns (stream ChatMessage);
}

message JoinRoomRequest {
  string username = 1;
  string room = 2;
}

message JoinRoomResponse {
  bool success = 1;
}

message SendMessageRequest {
  string username = 1;
  string room = 2;
  string message = 3;
}

message SendMessageResponse {
  bool success = 1;
}

message ReceiveMessagesRequest {
  string room = 1;
}

message ChatMessage {
  string username = 1;
  string message = 2;
  int64 timestamp = 3;
}

```
**We have to include the .proto file on the .csproj so we can enable the code generator**, so on the ChatAppServer.csproj file add the following:

```
<ItemGroup>
  <Protobuf Include="Protos\chat.proto" GrpcServices="Server" />
</ItemGroup>
```
And on the ChatAppClient.csproj add the following:

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

After this, if we clean, restore and build the projects, the code will be generated and you can check it out on the path "obj\Debug\net6.0\Protos".

#### Implement the server
In the server project, implement the ChatService.

```
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading.Tasks;
using ChatApp;
using Grpc.Core;
using Microsoft.Extensions.Logging;

namespace ChatAppServer.Services
{
    public class ChatServiceImpl : ChatService.ChatServiceBase
    {
        private static readonly ConcurrentDictionary<string, List<IServerStreamWriter<ChatMessage>>> _rooms = new ConcurrentDictionary<string, List<IServerStreamWriter<ChatMessage>>>();

        public override Task<JoinRoomResponse> JoinRoom(JoinRoomRequest request, ServerCallContext context)
        {
            if (!_rooms.ContainsKey(request.Room))
            {
                _rooms[request.Room] = new List<IServerStreamWriter<ChatMessage>>();
            }
            return Task.FromResult(new JoinRoomResponse { Success = true });
        }

        public override Task<SendMessageResponse> SendMessage(SendMessageRequest request, ServerCallContext context)
        {
            if (_rooms.TryGetValue(request.Room, out var clients))
            {
                var message = new ChatMessage
                {
                    Username = request.Username,
                    Message = request.Message,
                    Timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds()
                };

                foreach (var client in clients)
                {
                    client.WriteAsync(message);
                }
            }
            return Task.FromResult(new SendMessageResponse { Success = true });
        }

        public override async Task ReceiveMessages(ReceiveMessagesRequest request, IServerStreamWriter<ChatMessage> responseStream, ServerCallContext context)
        {
            if (_rooms.TryGetValue(request.Room, out var clients))
            {
                clients.Add(responseStream);
                try
                {
                    await Task.Delay(Timeout.Infinite, context.CancellationToken);
                }
                finally
                {
                    clients.Remove(responseStream);
                }
            }
        }
    }
}

```
And we need to add the following on the Progra.cs file:

```
builder.Services.AddGrpc();
app.MapGrpcService<ChatServiceImpl>();
```

#### Implement the client
Modify the Program.cs file of the client project:

```
using System;
using System.Threading.Tasks;
using Grpc.Net.Client;
using ChatApp;
using Grpc.Core;

class Program
{
    static async Task Main(string[] args)
    {
        // Create a channel to the gRPC server
        var channel = GrpcChannel.ForAddress("https://localhost:7187");
        var client = new ChatService.ChatServiceClient(channel);

        Console.WriteLine("Enter your username:");
        var username = Console.ReadLine();

        Console.WriteLine("Enter the room you want to join:");
        var room = Console.ReadLine();

        // Join the specified room
        var joinResponse = await client.JoinRoomAsync(new JoinRoomRequest
        {
            Username = username,
            Room = room
        });

        if (joinResponse.Success)
        {
            Console.WriteLine($"Joined room: {room}");
        }
        else
        {
            Console.WriteLine("Failed to join room");
            return;
        }

        // Task for receiving messages
        var receiveTask = Task.Run(async () =>
        {
            using var call = client.ReceiveMessages(new ReceiveMessagesRequest { Room = room });
            await foreach (var message in call.ResponseStream.ReadAllAsync())
            {
                Console.WriteLine($"[{message.Username}] {message.Message}");
            }
        });

        // Loop to send messages
        while (true)
        {
            var message = Console.ReadLine();
            if (message.ToLower() == "exit") break;

            var sendMessageResponse = await client.SendMessageAsync(new SendMessageRequest
            {
                Username = username,
                Room = room,
                Message = message
            });

            if (!sendMessageResponse.Success)
            {
                Console.WriteLine("Failed to send message");
            }
        }

        // Wait for the receiving task to complete
        await receiveTask;
    }
}

```

## Running the application
Use the **dotnet run** command to run the projects.
Open 2 terminals of clients to interact with each other.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2j6vq2nrz2bsxzi3recj.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/awbctzuv8ufntv8r8hw3.png)


## Conclusion
This is still a very simple case of implementing gRPC framework to develop an API, and I still have a lot to work on and learn about this, I'm very excited!

And that's it guys, I hope you liked it, stay tuned for more!