facebook

Blog

Stay updated

Let's see how Redis allows you to horizontally scale ASP.NET Core SignalR applications with Blazor
Redis as Backplane to scale your Blazor applications
Wednesday, February 24, 2021

In a previous article, we saw that it is possible to use technology such as Redis in its most common use, that is, as a database or cache and as a messaging system, by taking advantage of its publish/subscribe mechanism.

This feature, combined with its versatility, makes sure that we can use it in various situations. In particular, as mentioned, it can be used to scale ASP.NET Core SignalR applications horizontally.

ASP .NET Core SignalR is a library that allows a server to send information to clients with no further requests to update the information, thus making the application real-time. SignalR stands for WebSocket, a full-duplex communication protocol, of which it is an abstraction.

SignalR implements a very interesting mechanism to do this. At the time of the negotiation between client and server, it decides the communication mechanism to use, based on their characteristics and compatibility. The mechanism can be the most performing one, as WebSocket, or the least performing, as Server-Sent Events, and Long Polling, considered fallback mechanisms.

The supported communication mechanisms are: 

  • WebSockets – This is a two-way connection established after a HTTP negotiation initiated by the client. If the server accepts, it happens that a bidirectional channel was madre, on which it can send data to the client (Push Model) without the latter having to request them every time
  • Server Sent Events – This is a one-way connection established after the HTTP negotiation initiated by the client. The one-way channel is from the server to the client, and the latter cannot make further requests. When necessary, the server sends events to the client which contain updated data.
  • Long polling – It is the oldest technique among the three, and it involves the creation of a connection that remains open until the server does not have the answer to send to the client. Once it has data to send, the connection is interrupted and the client will contact the server again to receive future updates.

The best way to understand some aspects of this library is through an example. 

You can choose among several frameworks and libraries to build the front end for an ASP.NET Core application. I chose Blazor, a framework for creating web interfaces using C #, one of the latest Microsoft innovations. 

Being a beginner with both ASP.NET Core SignalR and Blazor, I took a cue from the Microsoft documentation where we can find a tutorial to create one of the simplest real-time applications that can come to our mind: a chat.

There are two hosting models for a Blazor application:

  • Blazor Web Assembly – The application with its dependencies and the .NET runtime runs within the browser itself and event handling and interface updates occur within the same process.
  • Blazor Server – the application in ASP.NET Core runs on a server. Event management and client interface updates take place via a SignalR connection.

For our example we will choose the latter.

First, we create a new project with the command:

dotnet new blazorserver -o ProjectName

Where ProjectName is the name we want to give to the project.

After that, we will find an already working application from which, however, we removed the related sample pages and logic.

We will not show the entire project (you can find the link to the GitHub repository at the end of the article) but only the essential and relevant components to explain some fundamental concepts of SignalR.

Among these, there is surely the concept of Hub. A hub is a pipeline that allows a client to invoke the server methods to the server and client to invoke the methods enabling the real-time communication between the two sides.

We install the Microsoft.AspNetCore.SignalR.Client NuGet package within our project and create a hub that we will call ChatHub.

using System; 

using System.Threading.Tasks; 

using Microsoft.AspNetCore.SignalR; 

namespace BlazorWithRedisBackPlane.Chat 

{ 

    public class ChatHub: Hub 

    { 

        public const string HubUrl = "/chat"; 

  
        public async Task SendMessageToAll(string username, string message) 

        { 

            await Clients.All.SendAsync("ReceiveMessage", username, message); 

        } 

  
        public override Task OnConnectedAsync() 

        { 

            Console.WriteLine($"{Context.ConnectionId} connected"); 

            return base.OnConnectedAsync(); 

        } 


        public override  async Task OnDisconnectedAsync(Exception exception) 

        { 

            Console.WriteLine($"Disconnected {exception?.Message} {Context.ConnectionId}"); 

            await base.OnDisconnectedAsync(exception); 

        } 

     } 

} 

As we can see, this class inherits from the base class Hub of SignalR and then defines the hub we will use in this application.

We define a HubUrl field that will represent the route to our hub.

The first of the three methods defined is SendMessageToAll(): we will use it to determine what will be done when a chat user sends a message.

We use the Clients object, also provided by SignalR, which allows us to invoke methods on clients connected to this hub. With Clients.All we indicate that the method is invoked on all clients connected to the hub while using the SendAsync() method we are going to specify the name of the method invoked for all clients connected to the hub.

The other two methods are override of the methods of the base class Hub.

OnConnectedAsync() is invoked when a new connection is established with the hub while when this connection ends the OnDisconnectedAsync() method is invoked.

In both we use the Context object of the library which contains information related to the connections in place such as the ConnectionId.

Let’s now define the interface and logic of our client. As mentioned earlier, when we create a new project, we are provided with something already working with example pages.

Some of these have been removed and the code within the Index.razor page has been replaced to create our chat.

@page "/" 

@inject NavigationManager navigationManager 

@using Microsoft.AspNetCore.SignalR.Client; 

@using BlazorWithRedisBackPlane.Chat; 

 
@if (!_isChatting) 

{ 

    <p> 

        Enter your name:  

    </p> 

  
    <input type="text" maxlength="32" @bind="@_username"/> 

    <button type="button" @onclick="@Chat"><span class="oi oi-chat" aria-hidden="true"></span> Start a chat </button> 

} 

else 

{  

    <div> 

        <span>User: <b>@_username</b></span> 

    </div> 

    <div id="scrollbox"> 

        @foreach (var item in _chatMessages) 

        { 

            <div class="@item.Style"> 

                <div class="user">@item.Username</div> 

                <div class="msg">@item.Body</div> 

            </div>             

        } 

        <hr /> 

        <textarea class="input" placeholder="enter your message" @bind="@_newMessage"></textarea> 

        <button class="btn btn-default" style="border-color:grey" @onclick="@(() => SendAsync(_newMessage))">Send</button> 

    </div> 

}
@code { 

    private bool _isChatting = false; 

    private string _username; 

    private string _message; 

    private string _newMessage; 

    private List<Message> _chatMessages = new List<Message>(); 

    private string _chatHubUrl; 

    private HubConnection _hubConnection; 

  

    public async Task Chat() 

    { 

         if (string.IsNullOrWhiteSpace(_username)) 

        { 

            _message = "Please enter a name"; 

            return; 

        }; 

          

        try 

        { 

            _isChatting = true; 

            await Task.Delay(1); 

  

            _chatMessages.Clear(); 

            _chatHubUrl = navigationManager.BaseUri.TrimEnd('/') + ChatHub.HubUrl;  

  

            _hubConnection = new HubConnectionBuilder() 

                .WithUrl(_chatHubUrl) 

                .Build(); 

  

            _hubConnection.On<string, string>("ReceiveMessage", HandleMessage); 

  

            await _hubConnection.StartAsync(); 

        } 

        catch (Exception e) 

        { 

            _message = $"ERROR: Failed to start chat client: {e.Message}"; 

            _isChatting = false; 

        } 

    } 

  
    private void HandleMessage(string name, string message) 

    { 

        bool isMine = name.Equals(_username, StringComparison.OrdinalIgnoreCase); 

  

        _chatMessages.Add(new Message(name, message, isMine)); 

  
        // Inform blazor the UI needs updating 

        StateHasChanged(); 

    } 

  
    private async Task SendAsync(string message) 

    { 

        if (_isChatting && !string.IsNullOrWhiteSpace(message)) 

        { 

            await _hubConnection.SendAsync("SendMessageToAll", _username, message); 

  
            _newMessage = string.Empty; 

        } 

  
    } 

  
    private class Message 

    { 

        public Message(string username, string body, bool mine) 

        { 

            Username = username; 

            Body = body; 

            IsMine = mine; 

        } 

  
        public string Username { get; set; } 

        public string Body { get; set; } 

        public bool IsMine { get; set; } 

        public string Style => IsMine ? "sent" : "received"; 

    } 

} 

The file is clearly divided into two parts that interact with each other. The first part defines the layout of the page in HTML and the logic that we can use to represent the various components within it using the Razor syntax.

With @code Directive, instead, we indicate the portion of code that can define the attributes and methods of the class that will be generated at compile time, and that will take the file name.

The interesting parts of this class are the three methods Chat(), HandleMessage() and SendAsync().

The first one is invoked when users enter their username and initializes the chat.

With the help of a HubConnectionBuilder() we are going to create a HubConnection that allows us to invoke the methods of the hubs in a SignalR Server.

In fact, with the statement:

_hubConnection.On<string, string>("ReceiveMessage", HandleMessage); 

a handler named HandleMessage() is registered, which will be invoked following the invocation of the ReceiveMessage method defined in the hub.

Finally, the connection to the server is initialized with the _hubConnection.StartAsync() method.

When the user enters a message, the SendAsync() method is invoked which in turn will invoke the specified hub method which in this case is “SendMessageToAll“.

This means that, since in the hub the “SendMessageToAll” method triggers the Client “ReceiveMessage” method that we connected to the HandleMessage() handler. The latter will then be executed, and it will only add the one entered by the user to the list of messages.

Finally, we have to add in the Startup.cs file and in particular in the EndPoints configuration that relating to ChatHub we defined.

endpoints.MapHub<ChatHub>(ChatHub.HubUrl); 

We run the application to test our chat in two browser windows:

Once the two users have been entered, we send a message to each side.

As we can see, the chat works correctly.

In the example shown, we used two browser instances for the two clients but the server application is always the same.

In the case of real-time applications or, in general, when an application has to serve a large number of clients, the application needs to be scaled horizontally. In this case, it can be a problem because, as we have seen, SignalR makes sure that the server manages the connections and therefore we would find ourselves in the situation described in the figure:

where the instances of SignalR on the various servers have no knowledge of the connections on the others.

To give an example closer to the real case, let’s create a Docker image for our application and instantiate two containers within a Docker network.

To create the Docker image of our application, we can simply generate the dockerfile using the official Docker extension for Visual Studio Code or the utility provided by Visual Studio for Windows, Docker Support.

Before proceeding, however, we need to make a small change to “bypass” a problem related to the redirection of HTTPs that does not allow us to start the connection to the server (https://github.com/dotnet/dotnet-docker/issues/2129).

In the Startup.cs file you have to comment the statement:

app.UseHttpsRedirection(); 

In addition, within Index.razor it is necessary to specify the URL of the connection to the ChatHub as follows:

string baseUrl = "http://localhost"; 

_chatHubUrl = baseUrl + ChatHub.HubUrl; 

We can then create the image of our application by running the command: 

docker build -t blazorchat:1 . 

Let’s create a docker network now:

docker network create --subnet=192.168.0.0/16 chat-network 

and two containers, assigning IP addresses within the network we just created 

Server1

docker run -h server1 --net chat-network --ip 192.168.0.5 -p 5000:80 --name server1  blazorchat:1

Server2

docker run -h server2 --net chat-network --ip 192.168.0.6 -p 5001:80 --name server2  blazorchat:1

If, as in the previous example, we connect to the address http://localhost: 5000/

using two different browser windows we get exactly the same result.

But if we use Server1 for one client (http://localhost: 5000/) and Server2 for the other client (http://localhost:5001/) we will have this situation

The two clients are not communicating: this is exactly what we were trying to describe before, namely that SignalR connections are independent between the various servers.

To overcome this situation, we have several solutions, including the use of Redis as BackPlane. The term backplane comes from electronics and refers to a set of connectors connected in parallel with each other so that each pin of one is connected to the corresponding pins of the others, thus forming a communication bus (https://en.wikipedia. org/wiki/Backplane). Similarly, Redis can be used to communicate the different nodes on which the SignalR application is running.

The application sends to the backplane both information related to client connections and the various messages received.

Through a publish/subscribe mechanism, the Redis backplane has all the information to correctly route all messages between all clients and servers to which they are connected.

Let’s try to reproduce the scenario of the previous example again, this time introducing Redis as a backplane.

We instantiate a container based on the Redis image within the Docker network defined above.

docker run -h redis --net chat-network --ip 192.168.0.7 --name redisbackplane -p 6379:6379 -d redis 

We add the Microsoft.AspNetCore.SignalR.StackExchangeRedis NuGet package to our application and once installed we add.within the method ConfigureServices(), the statement

services.AddSignalR().AddStackExchangeRedis("192.168.0.7"); 

Since we modified the application, let’s create a new image

docker build -t blazorchat:2 . 

Let’s create the two containers again, this time adding the Redis server as host:

Server1

docker run -h server1 --net chat-network --ip 192.168.0.5 --add-host redis:192.168.0.7 -p 5000:80 --name server1 blazorchat:2

Server2

docker run -h server2 --net chat-network --ip 192.168.0.6 --add-host redis:192.168.0.7 -p 5001:80 --name server2 blazorchat:2 

In this case, if we connect to Server1 (http://localhost:5000/) and Server2 (http://localhost:5001/) and try our chat we will finally have the communication between the two instances of our application.

We can also see the activity on the backplane by connecting to the Redis container and using the command:

redis-cli monitor

I hope it was interesting to see another way to take advantage of the versatility of Redis and use it to help us scaling our application.

You can find the project code with the dockerfile at the following link.

Furthermore, if you are interested in Blazor, I invite you to follow the Blazor Developer Italiani, community, founded by our CEO Michele Aponte, and the conference BlazorConf2021 scheduled for March.

See you at the next article!

Written by

Genny Paudice