Get in touch
Thank you
We will get back to you as soon as possible
.pdf, .docx, .odt, .rtf, .txt, .pptx (max size 5 MB)

10.5.2023

10 min read

Real-Time Made Easy: An Introduction to SignalR

Microsoft is not perfect. .NET is a great… thing, but what exactly is it? It’s evolving rapidly, but explaining .NET, .NET Framework, .NET Core, and again .NET to someone who is not in the context, could create even more confusion. Its set of tools is truly impressive, but you will most likely discover a large portion of its capabilities from random articles on the internet or videos on YouTube. The official documentation has probably already crossed the point of no return, making it impossible to refactor and reorganize it without going insane.

And yet, sometimes the stars converge, and we get something as impressive as SignalR, also made by Microsoft: powerful, simple, and intuitive. What is SignalR? It’s a library that makes the need to work directly with Websockets in ASP.NET almost redundant.

SignalR is a library for real-time communication that provides a very concise, yet powerful API that covers most real-time communication cases. Here are the strongest features of the library:

  • Nice abstractions. It’s so easy to get used to the concepts of hubs, methods, clients, etc. It’s very intuitive.
  • Connection management is handled automatically. This means that connections, disconnections, and reconnections are something that SignalR does for you. However, there is some granularity in configuring parts of it.
  • It scales perfectly and is production-ready. For example, it’s used in the new Bing Chat to deliver results, and one can only imagine the overall amount of those
  • It’s not just WebSockets. I have a feeling that for many people, SignalR is associated exclusively with WebSockets, which is not correct because, under the hood, it supports multiple transports that the library can switch between automatically if needed. For example, if the client doesn’t support WebSockets, it’ll automatically fall back to Server-Sent Events and Long Polling. It leaves you with the same real-time functionality, which is handled automatically by the sides’ capabilities.

It is ironic that SignalR is so powerful yet intuitive and concise that most of the tutorials on the internet are almost the same. Once, I tried to do something else and created a dummy alternative to the good old “Duck Hunt” (Duck Hunt (NES) Playthrough - NintendoComplete - YouTube), where you can manipulate a “character” on the screen with your phone. And the funny part is that my C# server code was only five lines. All the rest was happening on the client.

This article is not a comprehensive SignalR tutorial but an introduction to this mix of simplicity and power.

Before I start, it’s worth mentioning that before the current SignalR ASP.NET Core version, there was a regular version, which is still available as a separate package but not relevant to the scope of this article.

Server and clients

To make SignalR work, a server is required. That's easy. The server can be as short as a few dozen of lines of code, and you can even replace it with a fully-managed Azure Service. The latter doesn’t mean you can skip writing code, but it eliminates the need to worry about hosting and scaling, etc.

Once the server is set up, you can switch to clients. SignalR also provides outstanding capabilities in this regard. Not only does it support all modern browsers (including mobile versions), but it also offers official clients for .NET and Java. There are also unsupported/unofficial clients for C++ and Swift, but you get the idea.

In most cases, the target audience is browsers, and all of them are supported.

SignalR server

I’d say we can have three different types of SignalR servers:

  • Managed Azure SignalR server
  • Dedicated self-hosted SignalR service as an independent component of your application
  • SignalR, as part of your existing backend (with a shared codebase and hosted as part of it)

Although the implementation logic remains unchanged regardless of the chosen approach, it still greatly affects your options for scaling this component of your application. I will describe this part a bit later.

First of all, let’s go over how to add SignalR to your project:

builder.Services.AddSignalR();

And that’s basically it. Then, of course, there is room for configuring such things as authentication, message formats, serialization, etc.

But after following the next step, you will already be able to connect with the clients and send messages back and forth.

Hubs

The central server concept of SignalR is the hub. And the name itself is quite self-explanatory. In SignalR, the hub is where all the messages arrive before being delivered to other clients or used by the server.

To create a hub, you need two things:

  1. A hub class
  2. A mapping to a specific endpoint that clients will use for connection

Hub class

To create a hub, you must create a dedicated class that either inherits from Hub or the generic Hub<T>. The main difference is that the generic Hub<T> provides type safety and is less error-prone. It doesn’t allow you to write the wrong method name.

This means that instead of writing something like this:

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

You can do this:

await Clients.All.ReceiveMessage(user, message);

Other than that, they are the same and provide the same functionality regarding real-time communication.

From now on, let’s assume we stick to the typed method and never manually write the method's name.

Now, there is an important part, that is easy but sometimes confuses people.

Let’s imagine we have the following class and interface:

public class ChatHub : Hub<IChatHub>
{
    public async Task SendMessage(Message message)
    {
        await Clients.All.ReceiveMessage(message);
    }
}

public interface IChatHub
{
    Task ReceiveMessage(Message message);
}

Everything defined in this example of a SignalR hub can only be called by the clients. Therefore, you cannot trigger this method from your server. On the other hand, everything defined in the _IChatHub_ is intended to be used by the server.

In this example, the _SendMessage_ method is called by the clients who want to send a message in the chat. And within the method's body, we call the server method, which just sends that message to all connected clients.

Everything defined in this example of a SignalR hub can only be called by the clients. Therefore, you cannot trigger this method from your server. On the other hand, everything defined in the _IChatHub_ is intended to be used by the server.

In this example, the _SendMessage_ method is called by the clients who want to send a message in the chat. And within the method's body, we call the server method, which just sends that message to all connected clients.

Hub mapping

After creating your hub, you need to map it, which is done in the Program.cs file like this:

app.MapHub<ChatHub>("/chat");

Your SignalR is now configured and ready to work. Now, the clients need to connect to it using this URL https://<host>:<port>/chat and call specific methods or wait for messages from the server. I’ll show the client code later, and now let’s discuss more about what needs and can be done for the hubs.

Calling client methods outside the hub

In the previous example, you saw how it’s possible to call the hub method within the hub, meaning how to do it in response to an explicit call from the client. However, there are many situations where you need to call it in other places in your code.

For example, after user A performs some action (triggering a REST API call), we want to notify user B about it. To achieve this, we’re using _IHubContext_ or _IHubContext<T>_. It’s just injected as a usual DI. And then, you can access most of the methods available in the hub itself, excluding, for example, _Caller_ (since there is no caller, obviously).

For instance, if we have a dedicated hub for sending notifications, we can use it like this: 

internal class SendMoneyCommandHandler
{
    private readonly IHubContext<NotificationsHub, INotificationsHub> _notificationsHub;

    public SendMoneyCommandHandler(IHubContext<NotificationsHub, INotificationsHub> notificationsHub)
    {
        _notificationsHub = notificationsHub;
    }

    public async Task Handle(SendMoneyCommand command)
    {
        ... command logic

        var notification = new Notification();

          _notificationsHub.Clients.User(command.receiverId)
              .NotifyUser(notification);
    }
}

What is happening here?

  1. We’re injecting the hub context
  2. Executing our business logic
  3. Creating the message (more on this later)
  4. And executing a strongly-typed method _NotifyUser_ that provides details.

We’re calling this method only for the user with a specified id, and the client is responsible for handling and displaying the incoming message. For example, it could be a pop-up displaying the transaction details.

You can inject _IHubContext_ anywhere, including directly into controllers, middleware, etc.

Connections, users, and groups in SignalR

In SignalR, there are three main categories of clients. Additionally, there are _All_ (which allows sending messages to basically everyone connected) and _Caller_ (available only in the hub), but you typically use these three categories. They are straightforward and very intuitive:

  • Connection is a connection. It represents a single connected client, which means that if the same authenticated user is connected from different devices, browsers, or even different tabs within a single browser, it’ll spawn two other connections
  • User is a single authenticated user. By default, the user is identified by the _ClaimTypes.NameIdentifier_ from the _ClaimsPrincipal_, but you can easily change the identifier if needed
  • Groups are manually created by .NET developers on the server and allow you to group connections (not users)

Working with the groups in SignalR is both easy and intuitive. For example, you don’t need to create, edit, or remove them. When you try to add the first user (following your conditions), the group will be automatically created for you. Likewise, when the last user leaves the group, it will be removed.

What is a SignalR client?

While SignalR requires a server to work, it’s useless without clients using it for communication. There are three main official libraries available for creating SignalR clients:

  • .NET client
  • Java client
  • JavaScript client

I will mostly refer to JavaScript/TypeScript clients since they are expected to be the most popular. The library can be installed with npm and can be used with a CDN or other options.

Similar to the server, there are numerous options, but the basic code can be summarized as follows:

const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chathub")
    .withAutomaticReconnect()
    .build();

async function start() {
    try {
        // subscription to methods should go here…

        await connection.start();

        // not here…

        console.log("SignalR Connected.");
    } catch (err) {
        console.log(err);
    }
};

// Start the connection.
start();
  1. Creating a connection using the _HubConnectionBuilder_
  2. Starting the connection

However, you would likely want to do more.

Handling hub methods on the client

Sending messages (or calling server methods) on the server is nice, but it’s easily achievable using conventional methods. On the other hand, calling clients from the server is the primary objective when using SignalR and similar solutions.

To make it work, you need to configure a single handler:

connection.on("ReceiveMessage", (user, message) => {
    const li = document.createElement("li");
    li.textContent = `${user}: ${message}`;
    document.getElementById("messageList").appendChild(li);
});

In this example, the client listens to the _ReceiveMessage_ method and adds a _li_ element with the message content to the DOM. In modern frameworks, this process may appear even easier and more intuitive, but the whole idea is that it’s enough to simply subscribe to a method and specify what needs to be done each time a new message is received. You must specify the same method name on the server and provide parameters. It’s crucial to register this before the start of the connection. It might work without it, but it won’t be stable.

Calling server methods from the client

The second use case is sending messages (or calling server methods) from the client. As already mentioned, this can be achieved using simple endpoints, but in some cases, there may be a need to use SignalR. For example, when endpoints are not available or when a specific action, such as adding a user to a group needs to be performed.

SignalR clients offer much more functionality beyond what has been discussed here. Even with the basic features, you have the ability to accomplish impressive tasks such as:

  • Statistics updates in real-time
  • Chat
  • Real-time notifications (displayed in popups and added to the list) without refreshing the page
  • And so much more

Hosting and scalability of SignalR ASP.NET Core version

While it may seem like an advanced topic, I’d like to mention it here and focus on two main points that I encountered when I first worked with SignalR.

These points become particularly relevant when you are not using Azure's managed service for hosting SignalR and have a load balancer in place.

Sticky sessions

Sticky sessions ensure that the server always uses the same server to communicate with a particular client. This is crucial because there may be cases where the client was initially connected to server A, but when server B tries to communicate back, it is unaware of the client’s connection. By maintaining sticky sessions, the communication flow remains seamless.

Scaling SignalR

Even with a simple chat application, there is a possibility that some users may not receive messages. This can happen when, for instance, user A, for example, sends a message that is handled by server A, and immediately sends this message to all users (but it knows nothing about the users connected to server B). Consequently, server B knows nothing about the new message, so its connections are unaware of this method.

SignalR-scale-no-backplane.png

Image source

To handle this, you’d need to use a backplane. There is an official one that utilizes Redis, but you can achieve the same goal using custom solutions, such as RabbitMQ, other message brokers, or any other suitable option.

Summary

In this article, I didn’t mention the following:

  • Almost nothing about the configuration of the server and client
  • Hub filters
  • Streaming capabilities
  • Security

My intention was to showcase how simple and intuitive this tool is so that you can start using it and fine-tune it for yourself.

As mentioned at the beginning, SignalR is one of the best libraries offered by Microsoft. It’s easy, intuitive, and powerful at the same time. In this article, I didn’t want to explain all the intricacies of using it but to introduce the technology and dispel some common misconceptions. I also wanted to help you avoid the mistakes I made when I first started working with SignalR, relying solely on the official documentation.

When this technology was relatively young, it was a nice toy to get acquainted with real-time communication, but now it’s a fully-fledged tool in the arsenal of modern specialists. At Binary Studio, you can hire SignalR developers who will help you develop a production-ready, scalable, and high-performance application with real-time communication capabilities.

0 Comments
name *
email *