Getting started with gRPC Service with .NET 7

gRPC, a high-performance, open-source RPC (Remote Procedure Call) framework developed by Google, has gained immense popularity among developers for building efficient and robust APIs. This blog post will explore how to get started with gRPC services using .NET 7.

What is gRPC?

gRPC is an open-source framework developed by Google that facilitates efficient and robust communication between distributed systems. It is based on the Remote Procedure Call (RPC) protocol, where a program can cause a procedure (subroutine) to execute in another address space (commonly on another server) without the programmer explicitly coding the details for remote communication. gRPC stands for Google Remote Procedure Calls.

Why was gRPC Created?

gRPC was created to address the need for a high-performance, language-agnostic, and platform-independent framework for building efficient APIs and microservices. It was designed to replace the traditional REST APIs and overcome their limitations. Some of the key reasons for its creation include:

  1. Efficiency: gRPC uses Protocol Buffers (protobufs) as its interface definition language (IDL). Protocol Buffers are a language-agnostic binary serialization format that is compact and efficient. This results in smaller message sizes and faster serialization/deserialization processes compared to JSON or XML used in REST APIs.

  2. Performance: gRPC supports multiplexing, where multiple requests can be sent concurrently over a single TCP connection. This feature significantly improves performance, especially in scenarios with high-latency connections, making it ideal for microservices architectures.

  3. Language Independence: gRPC supports many programming languages, making it easy to build polyglot systems where different services are implemented in other languages but can still communicate seamlessly.

  4. Streaming: gRPC supports unary (single request/response) and streaming RPCs. Bidirectional streaming allows clients and servers to send a stream of messages to each other, enabling real-time communication patterns.

  5. Code Generation: gRPC uses Protocol Buffers to define service methods and message types. This definition can be used to generate client and server code in multiple programming languages, ensuring type safety and reducing the chance of errors.

Key Concepts in gRPC:

  1. Service Definition: gRPC services are defined using Protocol Buffers. A .proto file defines the service methods and the message types used in the service.

  2. RPC Methods: gRPC services expose remote methods clients can call. These methods are defined in the .proto file and can be unary or streaming.

  3. Protocol Buffers (protobufs): Protocol Buffers are used to serialize structured data. They provide a language-agnostic interface definition, allowing you to define your data models and service methods.

  4. Streaming: gRPC supports streaming, enabling clients and servers to send and receive a stream of messages, making it suitable for real-time applications.

  5. Code Generation: Protocol Buffers are compiled to generate client and server code in various programming languages. This generated code handles the serialization/deserialization and network communication, making it easy for developers to work with gRPC.

Implementation

First, we will create the gRPC project with the following command.

dotnet new grpc -n MyGrpcService

Nuget packages

We will use the following nuget packages:

  1. Grpc.Tools: provides tools and utilities for working with gRPC in .NET applications. It includes the Protocol Buffers compiler (protoc) plugin for C# code generation. This plugin is essential for generating C# client and server code from the .proto files (Protocol Buffers files), which define the gRPC service methods and message types.

  2. Microsoft.AspNetCore.Grpc.JsonTranscoding: creates RESTful JSON APIs for gRPC services.

Define a .proto File

Defining services in gRPC involves creating a Protocol Buffers (.proto) file where we specify the service methods and message types used for communication. Protocol Buffers provide a language-agnostic way to define our API, allowing us to generate client and server code in multiple programming languages.

The following proto has:

  • CreateMuscleRequest and ReadMuscleResponse are message types. Message types define the data structure that can be sent or received in gRPC methods.

  • CoachPlanService is the service containing the RPC method CreateMuscle. rpc CreateMuscle(CreateMuscleRequest) returns (ReadMuscleResponse) {} defines a unary RPC method called CreateMuscle that takes a CreateMuscleRequest message as input and returns a ReadMuscleResponse message.

  • In the context of Protocol Buffers (protobuf), "repeated" is a keyword used to define repeated fields within a message. Repeated fields are used to represent a list of values of the same type. They allow us to have multiple occurrences of a field within a single message

  • When creating the fields of the message, it is possible to define the order of the fields.

syntax = "proto3";

option csharp_namespace = "CoachPlan.gRPC";

service CoachPlan {
    rpc CreateMuscle(CreateMuscleRequest) returns (ReadMuscleResponse){

    }

    rpc ReadMuscle(ReadMuscleRequest) retuns (ReadMuscleResponse) {

    }

    rpc ListMuscle(GetAllMuscleRequest) returns (GetAllMuscleResponse) {

    } 

    rpc UpdateMuscle(UpdateMuscleRequest) returns (ReadMuscleResponse) {

    }

    rpc DeleteMuscle(DeleteMuscleRequest) returns (DeleteMuscleResponse) {

    }    
}

message CreateMuscleRequest {
    string name = 1;
}

message ReadMuscleRequest {
    int32 id = 1;
}

message ReadMuscleResponse {
    int32 id = 1;
    string name = 2;
}

message GetAllMuscleRequest {}

message GetAllMuscleResponse {
    repeated ReadMuscleResponse muscles = 1;
}

message UpdateMuscleRequest {
    int32 id = 1;
    string name = 2;
}

message DeleteMuscleRequest {
    int32 id = 1;
}

message DeleteMuscleResponse {
    int32 id = 1;
}

Service implementation

Before implementing the service, we must add the coachPlan.proto in the csproj file and build the project.

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

After building the project, a file containing an abstract class with the methods defined in the proto file will be generated. We will inherit our service from this class and override the methods.

/// <summary>Base class for server-side implementations of CoachPlan</summary>
    [grpc::BindServiceMethod(typeof(CoachPlan), "BindService")]
    public abstract partial class CoachPlanBase
    {
      [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
      public virtual global::System.Threading.Tasks.Task<global::CoachPlan.gRPC.ReadMuscleResponse> CreateMuscle(global::CoachPlan.gRPC.CreateMuscleRequest request, grpc::ServerCallContext context)
      {
        throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
      }

      [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
      public virtual global::System.Threading.Tasks.Task<global::CoachPlan.gRPC.ReadMuscleResponse> ReadMuscle(global::CoachPlan.gRPC.ReadMuscleRequest request, grpc::ServerCallContext context)
      {
        throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
      }

      [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
      public virtual global::System.Threading.Tasks.Task<global::CoachPlan.gRPC.GetAllMuscleResponse> ListMuscle(global::CoachPlan.gRPC.GetAllMuscleRequest request, grpc::ServerCallContext context)
      {
        throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
      }

      [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
      public virtual global::System.Threading.Tasks.Task<global::CoachPlan.gRPC.ReadMuscleResponse> UpdateMuscle(global::CoachPlan.gRPC.UpdateMuscleRequest request, grpc::ServerCallContext context)
      {
        throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
      }

      [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)]
      public virtual global::System.Threading.Tasks.Task<global::CoachPlan.gRPC.DeleteMuscleResponse> DeleteMuscle(global::CoachPlan.gRPC.DeleteMuscleRequest request, grpc::ServerCallContext context)
      {
        throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, ""));
      }

    }

The following code shows the implementation of the service.

public class CoachPlanService : CoachPlan.CoachPlanBase
{
    private readonly IMuscleService _muscleService;

    public CoachPlanService(IMuscleService muscleService)
    {
        _muscleService = muscleService;
    }

     public override async Task<ReadMuscleResponse> CreateMuscle(CreateMuscleRequest request, ServerCallContext context)
     {
        if(string.IsNullOrEmpty(request.Name) || string.IsNullOrEmpty(request.Name))
            throw new RpcException(new Status(StatusCode.InvalidArgument, "The property 'Name' is required."));

        MuscleDto dto = request.ToDto();

        var id = await _muscleService.Create(dto);

        var muscleDto = await _muscleService.GetById(id);

        var response = muscleDto.ToGrpcModel();

        return response;
     }

     public override async Task<GetAllMuscleResponse> ListMuscle(GetAllMuscleRequest request, ServerCallContext context)
     {
        var muscles = await _muscleService.GetAll();

        var response = muscles.ToGrpcModel();

        return response;
     }

     public override async Task<ReadMuscleResponse> ReadMuscle(ReadMuscleRequest request, ServerCallContext context)
     {
         var muscle = await _muscleService.GetById(request.Id);

         if(muscle == null)
            throw new RpcException(new Status(StatusCode.NotFound, "Not Found"));


         return muscle.ToGrpcModel();
     }

      public override async Task<ReadMuscleResponse> UpdateMuscle(UpdateMuscleRequest request, ServerCallContext context)
      {
         if(string.IsNullOrEmpty(request.Name) || string.IsNullOrEmpty(request.Name))
            throw new RpcException(new Status(StatusCode.InvalidArgument, "The property 'Name' is required."));

         var muscle = await _muscleService.GetById(request.Id);

         if(muscle == null)
            throw new RpcException(new Status(StatusCode.NotFound, "Not Found"));

         muscle.Name = request.Name;

         await _muscleService.Update(muscle); 

         return muscle.ToGrpcModel();
      }

      public override async Task<DeleteMuscleResponse> DeleteMuscle(DeleteMuscleRequest request, ServerCallContext context)
      {
         var muscle = await _muscleService.GetById(request.Id);

         if(muscle == null)
            throw new RpcException(new Status(StatusCode.NotFound, "Not Found"));

         await _muscleService.Delete(muscle);

         return new DeleteMuscleResponse()
         {
            Id = muscle.Id
         };
      }
}

And then, we need to configure the service in the app.

// Configure the HTTP request pipeline.
app.MapGrpcService<GreeterService>();
app.MapGrpcService<CoachPlanService>();

Testing

We need to create a gRPC request to test the implementation with Postman. After that, it is necessary to import the proto file. On the next page, it is required to define a name for the API that will be imported. After that, the methods will be listed on the request page. The following images show the step-by-step.

We must pick a method from the list and define the message with the fields defined in the project's proto file to execute a method.

Wrapping Up

This was a simple introduction to building a gRPC with .NET. The seamless integration of gRPC with .NET brings the power of Google's innovative technology to the Microsoft ecosystem and highlights the importance of efficient, real-time, bidirectional communication. With its support for streaming, code generation, and multiplexing, gRPC simplifies the complexities of distributed systems, allowing developers to focus on building meaningful features instead of worrying about network intricacies.

You can find the complete code on my GitHub.