gRPC for .NET: Setting up gRPC Server Application (ASP.NET Core) & Client Application (.NET Core) – Part I: Creating a gRPC Server Application which includes service which implements Unary, Server streaming, Client streaming & Bi-directional streaming methods

Recently, while working with Protobuf message format for one of my client project, I came across with gRPC framework. After doing my analysis, gRPC seems pretty promising for inter-service communication particularly in microservices architectures. gRPC is a language agnostic, high-performance Remote Procedure Call (RPC) framework. gRPC is built on top of HTTP/2 transport layer and therefore can support 4 types gRPC methods (unary, client streaming, server streaming, and bi-directional streaming). It uses Protobuf for message exchange.

Note: For Part II of this post refer gRPC for .NET: Creating a gRPC Client Application & for more info on Protobuf refer Getting started with Protobuf using C# runtime library for Protocol Buffers

In this post, I will be explaining how we can quickly setup a gRPC Server Application.

Getting started with gRPC Server Application

Step 1: Add a new project & pick ASP.NET Core gRPC Service project template in Visual Studio

gRPC Server Project ASP.NET Core

gRPC Server Project File/Folder Organization

gRPC Server - Project Folder Structure

Step 2: Add CommandProcessor.proto in the Protos root folder

CommandProcessor.proto contains following gRPC method types are:

  • Unary
  • Server streaming
  • Client streaming
  • Bi-directional streaming
syntax = "proto3";

option csharp_namespace = "SampleApplication.Grpc.Server";

package SampleCommandProcessor;

// The service definition.
service CommandProcessor {
  // Unary method
  rpc CommandUnary (CommandRequest) returns (CommandResponse);

  // Server streaming method
  rpc CommandServerStreaming (CommandRequest) returns (stream CommandResponse);

  // Client streaming method
  rpc CommandClientStreaming (stream CommandRequest) returns (CommandResponse);

  // Bi-directional streaming method
  rpc CommandBiDirectionalStreaming (stream CommandRequest) returns (stream CommandResponse);
}

// The request message containing the command's name.
message CommandRequest {
  string name = 1;
}

// The response message containing the message.
message CommandResponse {
  string message =2;
}

Step 3: Set CommandProcessor.proto file properties: Set Build Action to Protobuf compiler & gRPC Stub Classes to Server Only

gRPC Server - Set proto file properties Build Action - Protobuf compiler & gRPC Stub Classes - Server Only

Step 4: Build the gRPC Server Application to generate C# code from the CommandProcessor.proto file

Location of generated C# code: \obj\Debug\net5.0\Protos

gRPC Server - Generated C# Classes

Generated C# code contains:

  • An abstract base type (CommandProcessorBase abstract class) for each service
  • Classes for any messages (CommandRequset & CommandRequest class)

Step 5: Create a concrete implementation of CommandProcessorBase abstract class

Add CommandProcessorService.cs class in Services root folder. This service class will inherit from CommandProcessorBase abstract class, will override & provide the implementation for the unimplemented methods of CommandProcessorBase abstract class.

A. Unary method

  • Return single response after processing of single request
  • Call completes when the response is returned
  • Simple request/response

CommandUnary method:

public override async Task<CommandResponse> CommandUnary(CommandRequest request, ServerCallContext context)
{
  // TODO:: Write your custom "Command Processing Logic" here and return response.
  await Task.Delay(1000);
  return new CommandResponse { Message = $"{request.Name}:: Processed Successfully" };
}

B. Server streaming method

  • Multiple messages can be streamed back to the caller during processing of single request
  • Call completes when the method returns
  • Server pushing updates to clients periodically

CommandServerStreaming method:

public override async Task CommandServerStreaming(CommandRequest request, IServerStreamWriter<CommandResponse> responseStream, ServerCallContext context)
{
  // Multi-step request processing & sending out intermediate/partial responses until request is processed completely.
  // TODO:: Write your custom "Command Processing Logic" here and write response to stream.
  await Task.Delay(1000);
  await responseStream.WriteAsync(new CommandResponse { Message = $"{request.Name}:: Step 1 Completed" });
  await Task.Delay(1000);
  await responseStream.WriteAsync(new CommandResponse { Message = $"{request.Name}:: Step 2 Completed" });
  await Task.Delay(1000);
  await responseStream.WriteAsync(new CommandResponse { Message = $"{request.Name}:: Step 3 Completed" });
  await Task.Delay(1000);
  await responseStream.WriteAsync(new CommandResponse { Message = $"{request.Name}:: Processed Successfully" });
}

C. Client Streaming method

  • Request stream to the server & single response returned from the server
  • Call completes when a response message is returned
  • File upload from client side

CommandClientStreaming method:

public override async Task<CommandResponse> CommandClientStreaming(IAsyncStreamReader<CommandRequest> requestStream, ServerCallContext context)
{
  var response = new CommandResponse { };

  await foreach (var request in requestStream.ReadAllAsync())
  {
    // Multi request processing & single combined response at the end.
    // TODO:: Write your custom "Command Processing Logic" here and combine responses.
    await Task.Delay(1000);
    var res = new CommandResponse { Message = $"{request.Name}:: Processed Successfully" };
    response.Message += res.Message;
    response.Message += Environment.NewLine;
  }

  return response;
}

Bi-directional streaming method

  • Client and service can send messages to each other at any time
  • Call completes when the the method returns
  • Client and server sharing messages like a chat application.

CommandBiDirectionalStreaming method: 

public override async Task CommandBiDirectionalStreaming(IAsyncStreamReader<CommandRequest> requestStream, IServerStreamWriter<CommandResponse> responseStream, ServerCallContext context)
{
  await foreach (var request in requestStream.ReadAllAsync())
  {
    // TODO:: Write your custom "Command Processing Logic" here and write response to stream.
    await Task.Delay(1000);
    await responseStream.WriteAsync(new CommandResponse { Message = $"{request.Name}:: Processed Successfully" });
  }
}

Full Sample code CommandProcessorService.cs service

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Grpc.Core;
using Microsoft.Extensions.Logging;

namespace SampleApplication.Grpc.Server.Services
{
    public class CommandProcessorService : CommandProcessor.CommandProcessorBase
    {
        private readonly ILogger<CommandProcessorService> logger;

        public CommandProcessorService(ILogger<CommandProcessorService> logger) 
            => this.logger = logger;

        public override async Task<CommandResponse> CommandUnary(CommandRequest request, ServerCallContext context)
        {
            // Simple request & response.
            return await ProcessCommand(request);
        }

        public override async Task CommandServerStreaming(CommandRequest request, IServerStreamWriter<CommandResponse> responseStream, ServerCallContext context)
        {
            // Multi-step request processing & sending out intermediate/partial responses until request is processed completely.
            await ProcessIntermediateSteps(request, responseStream, 1);
            await ProcessIntermediateSteps(request, responseStream, 2);
            await ProcessIntermediateSteps(request, responseStream, 3);
            await ProcessCommand(responseStream, request);
        }

        public override async Task<CommandResponse> CommandClientStreaming(IAsyncStreamReader<CommandRequest> requestStream, ServerCallContext context)
        {
            var response = new CommandResponse { };

            await foreach (var request in requestStream.ReadAllAsync())
            {
                // Multi request processing & single combined response at the end.
                var res = await ProcessCommand(request);
                response.Message += res.Message;
                response.Message += Environment.NewLine;
            }

            return response;
        }

        public override async Task CommandBiDirectionalStreaming(IAsyncStreamReader<CommandRequest> requestStream, IServerStreamWriter<CommandResponse> responseStream, ServerCallContext context)
        {
            await foreach (var request in requestStream.ReadAllAsync())
            {
                await ProcessCommand(responseStream, request);
            }
        }

        private static async Task<CommandResponse> ProcessCommand(CommandRequest request)
        {
            // TODO:: Write your custom "Command Processing Logic" here and return response.
            await Task.Delay(1000);
            return new CommandResponse { Message = $"{request.Name}:: Processed Successfully" };
        }


        private static async Task ProcessIntermediateSteps(CommandRequest request, IServerStreamWriter<CommandResponse> responseStream, int stepCount)
        {
            // TODO:: Write your custom "Command Processing Logic" here and write response to stream.
            await Task.Delay(1000);
            await responseStream.WriteAsync(new CommandResponse { Message = $"{request.Name}:: Step {stepCount} Completed" });
        }

        private static async Task ProcessCommand(IServerStreamWriter<CommandResponse> responseStream, CommandRequest request)
        {
            // TODO:: Write your custom "Command Processing Logic" here and write response to stream.
            await Task.Delay(1000);
            await responseStream.WriteAsync(new CommandResponse { Message = $"{request.Name}:: Processed Successfully" });
        }
    }
}

Step 6: Add CommandProcessorService service to the routing pipeline with the MapGrpcService method in the Startup.cs

app.UseEndpoints(endpoints =>
{
  endpoints.MapGrpcService<CommandProcessorService>();
});

That’s It. gRPC Server Application is ready !!!

gRPC Server

 

You may also like...

1 Response

  1. July 15, 2021

    […] For Part I of this post refer gRPC for .NET: Creating a gRPC Server Application & for more info on Protobuf refer Getting started with Protobuf using C# runtime library for […]

Leave a Reply

Your email address will not be published. Required fields are marked *