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 doesn’t get 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
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 and intuitive yet concise. So much so 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” (
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.
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
In most cases, the target audience is browsers, and all of them are supported.
I’d say we can have three different types of SignalR servers:
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.
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:
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.
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.
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?
_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.
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:
_ClaimTypes.NameIdentifier_
from the _ClaimsPrincipal_
, but you can easily change the identifier if needed
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.
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:
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();
_HubConnectionBuilder_
However, you would likely want to do more.
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.
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:
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 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.
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.
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.
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
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.
Also published here.