C#/.NET gRPC Service with Duplex (Bidirectional) Streaming

One of the main advantage of gRPC over REST API’s is that gRPC supports streaming methods in addition to the traditional request-response style. There are 3 kinds of streaming support in gRPC namely Client Streaming, Server Streaming and Duplex (bi-directional streaming). In this post we talk about Duplex (Bidirectional) Streaming and how to implement it in C#/.NET

What is Duplex Streaming

In Duplex Streaming scenario, both the client and the server sends a sequence messages to each other via separate read and write streams. The call is initiated by the client to the server after which the streams will be available. The streams are independent from each other so the client and the server can read and write to the streams as required by their own applications’ requirements. For example, the server can wait for all the messages from the client before it sends back response, or it could immediately reply and have a “ping-pong”, chat-like, style of communication with the client. Within each stream, the order of messages is guaranteed.

A definition of a duplex streaming method is shown below. Note the use of the keyword stream in both the request and the response.

 rpc ChatNotification(stream NotificationsRequest) returns (stream NotificationsResponse);   

Implementation (Server)

To create a gRPC project follow the steps in Your First gRPC Project. By default, dotnet creates a greet.proto file which contains the service definition of the generated GreeterService. Let’s edit this file and replace its content with the proto file below. Also best to rename the service as well as the proto file. In this sample code, let’s rename the service from Greeter to Notifier and greet.proto to notify.proto

In this example implementation, we will do a “ping-pong” style of communication where the server responds to every request that the client makes.

syntax = "proto3";
import "google/protobuf/timestamp.proto";
option csharp_namespace = "DuplexStreaming";

package notify;


service Notifer {
   rpc ChatNotification(stream NotificationsRequest) returns (stream NotificationsResponse);   
}

message NotificationsRequest {
  string message = 1;
  string to = 2;
  string from = 3;
}

message NotificationsResponse{  
  string message = 1;
  google.protobuf.Timestamp receivedAt = 3;
}

Build the project/solution to ensure that everything is correct. Now, open up NotifierService.cs and add the implementation of the ChatNotifications method.

Note that the 1st parameter of the method is an IAsyncStreamReader<NotificationRequest> and the 2nd parameter is an IServerStreamWriter<NotificationResponse>

using System.ComponentModel;
using System.Diagnostics.Metrics;

using DuplexStreaming;

using Google.Protobuf.WellKnownTypes;

using Grpc.Core;

namespace DuplexStreaming.Services;
public class NotifierService : Notifier.NotifierBase {
    private readonly ILogger<NotifierService> _logger;
    public NotifierService(ILogger<NotifierService> logger) {
        _logger = logger;
    }

    public override async Task ChatNotification(IAsyncStreamReader<NotificationsRequest> requestStream, IServerStreamWriter<NotificationsResponse> responseStream, ServerCallContext context) {

        while (await requestStream.MoveNext()) {
            var request = requestStream.Current;
          
            var now = Timestamp.FromDateTimeOffset(DateTimeOffset.UtcNow);
            var reply = new NotificationsResponse() {
                Message = $"Hi {request.From}!, You have sent the message \"{request.Message}\" to {request.To}",
                ReceivedAt = now
            };
         
            await responseStream.WriteAsync(reply);
          
        }
    }
}

  • IAsyncStreamReader<T> requestStream
    – The client application writes to this stream to send a message to the server
  • IServerStreamWriter<T> responseStream
    – The server writes to this stream to send a message to the client. In our sample implementation above, the server sends back a response for every message it receives from the client (like a chat app)

On Line 19, we call MoveNext() on the requestStream and in Line 20, we take the latest message from client by accessing the Current property. We then use values from the request to immediately send back a response – which is done in Line 28 by call the WriteAsync method of the responseStream.

Implementation (Client)

using DuplexStreaming;
using Grpc.Net.Client;

// The port number must match the port of the gRPC server.
using var channel = GrpcChannel.ForAddress("http://localhost:5295");
var client = new Notifier.NotifierClient(channel);
using var call = client.ChatNotification();

var responseReaderTask = Task.Run(async Task () =>
{
    while (await call.ResponseStream.MoveNext(CancellationToken.None))
    {
        var note = call.ResponseStream.Current;
        Console.WriteLine($"{note.Message}, received at {note.ReceivedAt}");
    }
});

foreach (var msg in new[] {"Tom", "Jones"})
{
    var request = new NotificationsRequest() { Message = $"Hello {msg}", From = "Mom", To = msg };
    await call.RequestStream.WriteAsync(request);
}

await call.RequestStream.CompleteAsync();
await responseReaderTask;

Console.WriteLine("Press any key to exit...");
Console.ReadKey();
 

On the client side, we need to initiate a call (Line 7) to get hold of the request and response streams. After which we then setup a task to read the server responses (Lines 9 – 16) before writing messages to the request stream (Lines 18 – 22)

Now, run both the client and the server, and you should see something like this…

PS C:\Users\Erik\Source\Repos\grpctutorials\DuplexStreaming\source\DuplexStreamingClient\bin\debug\net8.0> .\DuplexStreamingClient.exe      
Hi Mom!, You have sent the message "Hello Tom" to Tom, received at "2024-01-25T10:07:32.183720200Z"
Hi Mom!, You have sent the message "Hello Jones" to Jones, received at "2024-01-25T10:07:32.183947200Z"
Press any key to exit...

Testing with FintX

You can also the service without needing to write a client application. Let’s fire up FintX (https://github.com/namigop/FintX) to verify that the service is working fine. FintX is an open-source, native, and cross-platform gRPC client. I’m on a Windows, so I have installed the Windows package (MacOS and Linux downloads are also available)

Click on the plus icon to add a client. The value entered in the http address should match the running service

Click Okay and then double-click on the ChatNotifications method to open it in a new tab. Test the method as per the short video below

Others

The full source code of this tutorial can be downloaded from https://github.com/namigop/grpctutorials/tree/main/DuplexStreaming. Happy coding!

C#/.NET gRPC Service with Client Streaming

One of the main advantage of gRPC over REST API’s is that gRPC supports streaming methods in addition to the traditional request-response style. There are 3 kinds of streaming support in gRPC namely Client Streaming, Server Streaming and Duplex (bi-directional streaming). In this post we talk about ClientStreaming and how to implement it in C#/.NET

What is ClientStreaming

In Client streaming calls the client sends an initial request to the server and gets back a stream to write a sequence of messages to. The client writes to the stream until it is finished and closes the stream. After which, the server responds with a final message containing the status code, status message, and optionally, some metadata in the Trailer/Header. Similar to server streaming, gRPC guarantees the order of messages that is written to the request stream.

A definition of a client streaming method is shown below. Note the use of the keyword stream in the request.

 rpc SendNotifications(stream NotificationsRequest) returns (NotificationsResponse);   

So why would you use ClientStreaming instead of just making a sequence of Unary calls? Well, writing to a stream is generally faster than making a new rpc each time. That said, streaming adds complexity in the implementation especially around the handling of error scenarios. For example, you’d need to handle in the your code what should happen when the connection is interrupted in the middle of a stream.

Implementation

To create a gRPC project follow the steps in Your First gRPC Project. By default, dotnet creates a greet.proto file which contains the service definition of the generated GreeterService. Let’s edit this file and replace its content with the proto file below

syntax = "proto3";
import "google/protobuf/timestamp.proto";
option csharp_namespace = "ClientStreaming";

package greet;


service Greeter {
   rpc SendNotifications(stream NotificationsRequest) returns (NotificationsResponse);   
}


// The request message containing the user's name.
message NotificationsRequest {
  string message = 1;
  string to = 2;
}

 

message NotificationsResponse{  
  string message = 1;
  int32 total = 2;
  google.protobuf.Timestamp receivedAt = 3;
}

Build the project/solution to ensure that everything is correct. Now, open up GreeterService.cs and add the implementation of the SendNotifications method. Note that the 1st parameter of the method is an IAsyncStreamReader<T>. This is what client applications use to write to the request stream.

using ClientStreaming;

using Google.Protobuf.WellKnownTypes;

using Grpc.Core;

namespace ClientStreaming.Services;
public class GreeterService : Greeter.GreeterBase {
    private readonly ILogger<GreeterService> _logger;
    public GreeterService(ILogger<GreeterService> logger) {
        _logger = logger;
    }

    public override async  Task<NotificationsResponse> SendNotifications(IAsyncStreamReader<NotificationsRequest> requestStream, ServerCallContext context) {
        int total = 0;

        List<NotificationsRequest> messages = new();
        while (await requestStream.MoveNext()) {
            var request = requestStream.Current;
            messages.Add(request);
            Console.WriteLine($"Message \"{request.Message}\" sent to {request.To}");
        }

        var recipients = messages.Select(c => c.To).Distinct().ToList();
        return new NotificationsResponse() { 
            Message = $"{string.Join(",", recipients)} received messages", 
            ReceivedAt = Timestamp.FromDateTimeOffset( DateTimeOffset.UtcNow ),
            Total = total
        };
    }
}

Hit F5 to run the project and take note of the port where it is running

Test the method

Now that the service is running, let’s fire up FintX (https://github.com/namigop/FintX) to verify that the service is working fine. FintX is an open-source, native, and cross-platform gRPC client. I’m on a Windows, so I have installed the Windows package (MacOS and Linux downloads are also available)

Click on the plus icon to add a client. The value entered in the http address should match the running service

Click Okay and then double-click on the SendNotifications method to open it in a new tab.

Click the Run button. If the response from the server is successful, the request stream panel will be enabled.

Enter the values to the request stream then click on the “Write to request stream” button. Write as many messages to the request stream as you want. Once finished, click on the “Finish writing to the request stream” button. The server responds with the final message (shown below).

Others

The full source code of this tutorial can be downloaded from https://github.com/namigop/grpctutorials/tree/main/ClientStreaming. Happy coding!

C#/.NET gRPC Service with Server Streaming

One of the main advantage of gRPC over REST API’s is that gRPC supports streaming methods in addition to the traditional request-response style. There are 3 kinds of streaming support in gRPC namely Client Streaming, Server Streaming and Duplex (bi-directional streaming). In this post we talk about Server Streaming and how to implement it in C#/.NET

What is Server Streaming

In Server streaming calls the client sends a request to the server and gets a stream to read a sequence of messages back. The client reads from the returned stream until there are no more messages. With gRPC, message ordering is guaranteed within an individual RPC call.. Once the client has received all the messages from the server, the server sends a final message containing the status code, status message, and optionally, some metadata in the Trailer/Header.

A definition of a server streaming method is shown below. Note the use of the keyword stream in the response.

rpc GetNotifications(GetNotificationsRequest) returns (stream GetNotificationsResponse);

So, why would one use server streaming over a Unary call that sends back the *entire* list of messages in the response? It seems to be just unnecessarily complex. In a way that is true, server streaming (or streaming in general) introduces additional complexity for both client and server side processing. It however affords several benefits and server streaming should be considered for the following scenarios

  • When high throughput is required
  • When low latency is required
  • When the size of the entire response payload is large enough to cause performance issues

Implementation

To create a gRPC project follow the steps in Your First gRPC Project. By default, dotnet creates a greet.proto file which contains the service definition of the generated GreeterService. Let’s edit this file and replace its content with the following

syntax = "proto3";
import "google/protobuf/timestamp.proto";


option csharp_namespace = "ServerStreaming";

package greet;

// The greeting service definition.
service Greeter {
    rpc GetNotifications(GetNotificationsRequest) returns (stream GetNotificationsResponse);
}

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

 

message GetNotificationsResponse{
  string name = 1;
  string message = 2;
  google.protobuf.Timestamp sent = 3;
}

Build the project/solution to ensure that everything is correct. Now, open up GreeterService.cs and add the implementation of the GetNotifications method. Note that the 2nd parameter of the method is an IServerStreamWriter<T>. This is what we will use to write to the response stream.

using Google.Protobuf.WellKnownTypes;

using Grpc.Core;

using ServerStreaming;

namespace ServerStreaming.Services;

public class GreeterService : Greeter.GreeterBase {
    private readonly ILogger<GreeterService> _logger;
    private readonly List<GetNotificationsResponse> _notifications;
    public GreeterService(ILogger<GreeterService> logger) {
        _logger = logger;
        
        //Sample data -  simulate DB
        this._notifications = new List<GetNotificationsResponse>();
        for (int i = 0; i < 100; i++) {
            this._notifications.Add(new GetNotificationsResponse() {
                Name = i % 2 == 0 ? "Joe" : "Mary",
                Message = $"Notification {i}",
                Sent = Timestamp.FromDateTimeOffset(DateTimeOffset.Now)
            });
        }
    }

    public override async Task GetNotifications(GetNotificationsRequest request, IServerStreamWriter<GetNotificationsResponse> responseStream, ServerCallContext context) {
        //simulate a db call
        var notifications = this._notifications.Where(m => m.Name == request.Name);

        foreach (var notif in notifications) {
            //write to the response stream
            await responseStream.WriteAsync(notif);
        }
    }
}

Hit F5 to run the project and take note of the port where it is running

Test the method

Now that the service is running, let’s fire up FintX (https://github.com/namigop/FintX) to verify that the service is working fine. FintX is an open-source, native, and cross-platform gRPC client. I’m on a mac, so I have installed the macOS package (Windows and Linux downloads are also available)

Click on the plus icon to add a client. The value entered in the http address should match the running service


I created 2 request files, one to hold the request for Joe and the other one for Mary. Note that our sample data holds notifications for either Joe or Mary. Open either *.fxrq file and click the Run button

And there it is! In the lower, right-hand side of the app (response stream panel ) the streamed 50 responses are displayed

Others

The full source code of this tutorial can be downloaded from https://github.com/namigop/grpctutorials/tree/main/ServerStreaming. Happy coding!

C#/.NET gRPC service with Unary Methods

Service Definitions and Protobuf

Similar to WCF (Windows Communication Foundation), gRPC relies on service definitions to specify what methods and data types are exposed by the service. Service definitions in gRPC are written in protocol buffers (or protobuf for short) whereas in WCF, a WSDL is used.

An example of a protobuf service definition is shown below. It defines a service named “HelloService” which has a single method called “SayHello“. SayHello accepts a request parameter of type HelloRequest and returns a response of type HelloResponse

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string greeting = 1;
}

message HelloResponse {
  string reply = 1;
}

Unary Methods

Unary methods are rpc (remote-procedure-call) calls wherein for every request, a response is sent back. It’s a lot similar to the normal function calls that we all write in our code except that for gRPC the calls go over the network.

In the above service definition, SayHello is a unary method.

Implementation

To create a gRPC project follow the steps in Your First gRPC Project. By default, Visual Studio creates a greet.proto file which contains the service definition of the generated GreeterService. The content of this greet.proto file is similar to the service definition a few paragraphs above.


Let’s create a second Unary method called GetCustomerByName by editing the greet.proto file

syntax = "proto3";

option csharp_namespace = "GrpcUnary";

package greet;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply);
  rpc GetCustomerByName (GetCustomerRequest) returns (GetCustomerResponse);
}

// The request message containing the user's name.
message GetCustomerRequest {
  string namePrefix = 1;
}

// The response message containing the greetings.
message GetCustomerResponse {
  repeated Customer customer = 1;
}

message Customer {
	string firstName = 1;
	string lastName = 2;
	int32 age = 3;
}

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

// The response message containing the greetings.
message HelloReply {
  string message = 1;
}

Press Ctrl-Shift-B to build the solution. If there are no errors in the proto file, the build should be successful.

Now that we have modified the service definition to include the GetCustomerByName method, we can go ahead and implement it in code. Open up GreeterService.cs and add an override for the method

public override async Task<GetCustomerResponse> GetCustomerByName(GetCustomerRequest request, ServerCallContext context)
{
    //Simulate a Db call to the Customers database
    var matched = _customers.Where(c => c.FirstName.StartsWith(request.NamePrefix));
    if (matched.Any())
    {
        var response = new GetCustomerResponse();
        response.Customers.AddRange(matched);
        return response;
    }

    throw new RpcException(new Status(StatusCode.NotFound, $"Unable to find a customer with a first name starting with {request.NamePrefix}"));
}


Press F5 to run the service and take note of the port where it is running


Test the method

Now that the service is running, let’s fire up FintX (https://github.com/namigop/FintX) to verify that the service is working fine. FintX is an open-source, native, and cross-platform gRPC client. I’m on Windows, so I have installed the windows package (macOS and Linux downloads are also available)

Click on the plus icon to add a client. The value entered in the http address should match the running service



Open GetCustomerByName method and edit the request. Double-click on the NamePrefix node to edit it then click the Run button


Give the NamePrefix some invalid value to verify that indeed an RPC exception is thrown with a NotFound status code


Others

The full source code of this tutorial can be downloaded from https://github.com/namigop/grpctutorials/tree/main/Unary. Happy coding!

Your First C# gRPC Project

It is super easy to get started with gRPC on .NET. Most of the heavy lifting is done by Visual Studio

Create the project

First, open Visual Studio and select the ASP.NET Core gRPC Service project template


then add the name of the project and click Next. Follow the wizard up to completion.


(Optional) Enable server reflection

After Visual Studio has created the project files, right-click on Dependencies then select Manage Nuget Packages.

Search for Grpc.AspNetCore.Server.Reflection and then click Install

Now open up Program.cs and let’s add a couple lines of code to enable server reflection. We do this on lines 7 and 14. We’ll need these later on to test that our service is working perfectly

using HelloGrpc.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddGrpc();
builder.Services.AddGrpcReflection();

var app = builder.Build();

// Configure the HTTP request pipeline.
app.MapGrpcService<GreeterService>();
app.MapGrpcReflectionService();
app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");

app.Run();


Run the gRPC service

Now, hit F5 (or click on the green Debug button) to run the services. Visual Studio will compile the project and then run it. As can be seen from the screenshot below, the service is hosted at port 7059 with an https endpoint and at 5013 for http

(Note: the port numbers will be different for your own machine)

That’s it! Your first gRPC Service is now up and running.


Test the gRPC service

Now let’s try to test it to see whether it can respond to http calls. For this we will need a test client. A great tool, and one that that I personally use, is FintX (https://github.com/namigop/FintX/). It’s an open-source, native and cross-platform gRPC client. I’m on Windows, so I have installed the windows package (macOS and Linux downloads are also available)

To add the our gRPC Service to FintX, select Reflection Service (this is why in the previous steps we enabled the reflection service) and add the other required fields.

Click Okay.

FintX will automatically generate the client code (C#) and compile it. Once, it is done it will display the gRPC methods in a treeview

Now, let’s invoke our gRPC service. Double-click the method “SayHello” or click on the open tab button. In the request editor, enter a name then click on the green Run button in the middle – FintX will now call our gRPC service.

And there you go, as can be seen from the screenshot below our service is working perfectly. We can see that it has sent back a response with the Name parameter that we passed in the request


Others

The full source code of this tutorial can be downloaded from https://github.com/namigop/grpctutorials/tree/main/GettingStarted. Happy coding!