Bi-directional streaming and introduction to gRPC on ASP.NET Core 3.0 (Part 2)

This is a second article in my short introduction to gRPC on ASP.NET Core 3, the first article is here. In this more practical part i’ve implemented a small console chat application, like customer support chat for example. The full codebase can be found in my Github Repo.
Now, let’s take a look into the code. gRPC service project can be easily created by using Visual Studio UI or with the command line command: dotnet new grpc -n YourGrpcService
In my solution code is in C# for both gRPC server and client. gRPC server is managing the customer connections and takes care about messages, broadcasting them to all the connected clients. Client is taking customer’s input, sends it to the server and also accepts messages from the server which were sent there by other clients.
We start with looking onto server code which is in CustomerGrpc project.But before I would like to point a few things in our standard Startup.cs and Program.cs files.
gRPC is enabled with the services.AdddGrpc();
method. This method is adding different services and middleware used to construct a pipeline for processing gRPC calls. Undercover method uses GrpcMarkerService class to make sure that all necessary gRPC services were added, it also adds some middleware which operates on the underlying HTTP/2 messages. You can also provide configuration for your services through the GrpcServiceOptions
type, which is later used in the pipeline. For example, the maximum incoming message size can be configured like that:
Then another interesting method for us is in the Endpoints routing: endpoints.MapGrpcService<Services.CustomerService>();
this method adds our gRPC service to the routing pipeline. In ASP.NET Core the routing pipeline is shared between middleware and features which means we can have additional request handlers there, such as MVC controllers. The additional request handlers work in parallel with the configured gRPC services. See below:
Now we need to configure Kestrel server.
Kestrel gRPC endpoints:
- Require HTTP/2.
- Should be secured with HTTPS.
HTTP/2
gRPC requires HTTP/2 and gRPC for ASP.NET Core validates that HttpRequest.Protocol is HTTP/2
.
Kestrel supports HTTP/2 on most modern operating systems. Kestrel endpoints are configured to support HTTP/1.1 and HTTP/2 connections by default.
HTTPS
Kestrel endpoints used for gRPC should be secured with HTTPS. In development, an HTTPS endpoint is automatically created at https://localhost:5001
when the ASP.NET Core development certificate is present. No configuration is required.
In production, HTTPS must be explicitly configured. In Our case we have next:
Please note, there’s known problem that macOS doesn’t support ASP.NET Core gRPC with Transport Layer Security (TLS). Additional configuration is required to successfully run gRPC services on macOS. This is exactly our case, that’s why we have to disable TLS with line
options.ListenLocalhost(5001, o => o.Protocols = HttpProtocols.Http2);
HTTP/2 without TLS should only be used during app development. Production apps should always use transport security. For more information, see Security considerations in gRPC for ASP.NET Core.
Now, let’s have a look on our gRPC Customer service. What we see at the beginning is that our CustomerService
class inherits from CustomerGrpc.CustomerService.CustomerServiceBase
class.
This class is generated based on our .proto file and if you F12 on it you’ll see a bunch of autogenerated code which describes our service, messages and communication.
In our class we override two custom methods defined in the parent class: JoinCustomerChat
and SendMessageToChatRoom
that’s is basically where all of our code is going. JoinCustomerChat is a demonstration of relatively simple request/restponse pattern where we send JoinCustomerRequest
with customer information and receive JoinCustomerReply
with a chat room RoomId
where customer has joined.
Then we have a bit more interesting SendMessageToChatRoom
method which is a demonstration of the bi-directional streaming.
Let’s take a closer look on the method’s parameters. IAsyncStreamReader<ChatMessage> requestStream
represents a stream of messages from the client which we can read and iterate through with requestStream.MoveNext()
. Method returns true if there is a message available and false if there are no more messages(i.e. the stream has been closed).
IServerStreamWriter<ChatMessage> responseStream
is also a stream of messages, this time it is writable and is used to send messages back to the client.
ServerCallContext context
provides some handy properties such as HttpContext of a call, HostName, etc.
The logics in the method isn’t complicated. When a call is received on the server, first we store the IServerStreamWriter<ChatMessage> responseStream
which is used to broadcast messages to the client, with
_chatRoomService.ConnectCustomerToChatRoom(requestStream.Current.RoomId, Guid.Parse(requestStream.Current.CustomerId), responseStream);
and then in a while(await requestStream.MoveNext())
loop we go through messages received from the client and broadcast them to another clients which are connected at the moment.
await _chatRoomService.BroadcastMessageAsync(requestStream.Current);
The code in the BroadcastMessageAsync()
method is going through all the connected client’s response streams and writes messages to the stream.
foreach (var customer in chatRoom.CustomersInRoom)
{
await customer.Stream.WriteAsync(message);
Console.WriteLine($"Sent message from {message.CustomerName} to {customer.Name}");
}
We also have a try/catch block to handle the connection failures and remove corresponding stream from our chat rooms.
catch (IOException)
{
_chatRoomService.DisconnectCustomer(requestStream.Current.RoomId, Guid.Parse(requestStream.Current.CustomerId));
_logger.LogInformation($"Connection for {user} was aborted.");
}
This is basically all server logics which is directly related to gRPC. I’ve created some additional functionality in services and wrappers which you can find here.
Now we can have a short look on the client code. First of all, we create GrpcChannel
and then feed it to the gRPC client.
var channel = GrpcChannel.ForAddress("http://localhost:5001", new GrpcChannelOptions { Credentials = ChannelCredentials.Insecure });var client = new CustomerService.CustomerServiceClient(channel);
Then client makesJoinCustomerChatAsync
which returns the RoomId of the chat room. Afterwards we open or bi-directional stream, by performing SendMessageToChatRoom
call and start messages exchange.

When the connection is opened we start a loop in the background task which reads messages from the response stream and then displays them to the console. The second loop is just taking the input from the keyboard and sends that input to the server.
Overall the code isn’t that complicated, but points a little where to look if there’s a need or design choice to implement your own gRPC service using .net core and c#.
GL&HF!