July 2018

Volume 33 Number 7

[Cutting Edge]

Online Users, Streaming and Other SignalR Goodies

By Dino Esposito | July 2018

Dino EspositoIf you ever used any version of SignalR for the classic ASP.NET platform, you should be quite familiar with the concept of a hub. In SignalR a hub is the component that enables compatible client and server applications to arrange bidirectional remote procedure calls, from client to server and from server back to connected clients.

Concretely, in terms of software, a hub is a class that inherits from a system-provided base class and exposes endpoints for clients to call. Conceptually speaking, it has a few points in common with an ASP.NET controller. In particular, it’s the façade that receives client calls and reacts to them, and is organized around a relatively small number of related functions. In ASP.NET Core SignalR, the similarity between hubs and controllers is, in a way, even closer. This is my third article about ASP.NET Core SignalR for the Cutting Edge column, and in neither of the previous two articles did I use a non-empty hub class. Explaining a bit more about SignalR hubs and how they’re implemented in ASP.NET Core is one of the purposes of this article. However, I’ll also touch on other interesting aspects of SignalR, specifically data streaming and the count of online users.

Hubs on ASP.NET Core SignalR

A hub is the entry point in a message pipeline through which connected clients and servers exchange messages. Hubs expose their methods as URLs and process any received parameters via model binding, in much the same way a canonical controller does. In a way, a hub is a dedicated controller that operates over two built-in protocols. The default protocol consists of a JSON object, but another binary protocol can be used that’s based on MessagePack. Note that in order to support MessagePack, browsers must support XHR Level 2. As Level 2 was introduced back in 2012, this might not be much of an issue today, but if your application requires support for very old browsers it might be worth noting. A quick browser check can be made here: caniuse.com/#feat=xhr2.

If any of the connected clients request a SignalR endpoint, the hub is directly invoked by the SignalR runtime engine. The conversation takes place over the selected protocol, mostly likely WebSockets. In order to invoke the server, clients need to hold a connection object. A Web client would get it as follows:

var clockConnection = new signalR.HubConnection("/clockDemo");
clockConnection.start().then(() => {

A client invokes a server endpoint via the “invoke” method on the connection object. Note that the exact syntax may vary depending on the actual technology used for the client. The server replies through the methods defined on the base Hub class, and the conversation takes place over the transport protocol of choice, most often WebSockets, like this:

public class ClockHub : Hub
  public Task Now()
    var now = DateTime.UtcNow.ToShortTimeString();
    return Clients.All.SendAsync("now", now);

Note that you won’t be able to monitor the various calls using an HTTP tool like Fiddler. You need something like Chrome Developer Tools. In all the examples I wrote for my past columns on SignalR, however, I always used an empty hub class.

The hub class is the official SignalR façade for receiving client calls, and the fastest way for client/server communication to take place because of the dedicated pipeline. When the call occurs through the hub, the SignalR runtime can track and expose all the available information via the properties of the base Hub class. In this way, the SignalR connection ID and the entire context of the call, including any claims of the authenticated user, are available.

In addition, through the hub, developers can handle connect and disconnect events. Any time a new connection is set up, or ceased, a hub method is called back. If you use SignalR only as a way to monitor remote long-running operations, then you can also trigger the task via a plain controller and inject a hub context in it for notifications. Or as an alternative, you can invoke the hub and trigger the task from within the hub. It’s your choice. SignalR works as an end-to-end framework, a bridge between the client and the server. Coding logic into the hub is acceptable as long as the work doesn’t go too deep into the layers of your code. Otherwise, go through the controller—whether MVC or Web API—and inject a hub context.

The only difference between using a hub or a controller is that SignalR can’t track the connection ID when a request goes through the controller. If the connection ID is relevant to the server task, then it has to be passed in some way via the URL. All other information that forms the SignalR caller context can be retrieved by the controller via the HTTP request context.

Counting Online Users

Some Web applications find it useful, or just engaging for users, to show how many connections are currently active. The problem is not so much tracking when a user connects—there are many endpoints through which you can detect that—but rather when a user disconnects from the application.

You can audit a login page or the post-authentication step. You can place a check in some base controller class or in any of the pages that the user can visit. In general, you can always find a way to detect when a user connects to the application. The problem is how the user can leave the application, whether by logging out (easily traceable) or by navigating away from the page or by shutting down the browser window. There’s no reliable way to detect when the user closes the browser window. Yes, browsers usually fire the beforeunload event when the browser shuts down, but this same event is also fired whenever you follow a link—even when that link is within the same application. So it’s not a perfect solution.

A much more reliable way to count users is to keep track of ASP.NET Core SignalR connections. To do this, you need a fully functional hub with the connection set up through it. When the user leaves the browser, or just the application, the connection is released and listening clients notified. As in ASP.NET Core SignalR, there’s no support for automatic reconnections, so things are even easier. All you do is define a global static variable in the hub and increment its value up or down when a user connects or disconnects, as shown in Figure 1. The SignalR runtime in ASP.NET Core ensures that every connection is closed at some point, and any new connection effectively refers to a new connection. In short, the number you get is highly reliable.

Figure 1 Counting Connections

public class SampleHub : Hub
  private static int Count = 0;
  public override Task OnConnectedAsync()
    Clients.All.SendAsync("updateCount", Count);
    return Task.CompletedTask;
  public override Task OnDisconnectedAsync(Exception exception)
    Clients.All.SendAsync("updateCount", Count);
    return Task.CompletedTask;

There’s one drawback to counting users with SignalR: It only works if users visit the page that establishes a connection to the hub where counting takes place. To be on the safe side, you need to have a SignalR client in nearly any page the user can visit. This is especially true if you consider that normally the number of online users is a globally visible value you probably have in all layouts on which your views are based.

Note that in the sample hub code, the class calls back the connected clients every time a connection is created or closed. Note also that in this way you only have the total number of users, but not the list of connection IDs or, in case of authenticated users, the list of user names. To achieve this, you better use a dictionary instead of a global counter and add to it entries with connection IDs or claims, such as the user name.

Another point to consider with reference to the code in Figure 1 is the use of a static variable to count users. A static variable is per-server, which means that when you scale out you’ll need to consider how to store shared state in a globally accessible location, such as a database or a distributed cache.

Pushing Information Back

From within the hub, or the hub context if you connect to the back end via a controller method, you have many different ways to call back connected clients. All methods are members exposed by the Clients object that, in spite of the name, is not a collection, but an instance of the IClientProxy class. The expressions in Figure 2 indicate the object from which the SendAsync method is invoked. The SendAsync method takes the name of the client method to call back and the parameters to pass.

Figure 2 Methods for the Server to Call Back Connected Clients

Expression Description
Clients.All The notification is broadcast to all connected clients, regardless of the technology being used (Web. .NET, .NET Core, Xamarin).
Clients.Client(connectionId) The notification is sent exclusively to the client listening over the specified connection.
Clients.User(userId) The notification is sent to all clients whose authenticated user matches the provided user name.
Clients.Groups(group) The notification is sent to all clients belonging to the specified group.


A group is a collection of related clients collectively gathered under a name. The more natural way of thinking of groups in SignalR is chat rooms. A group is created programmatically simply adding connection IDs to the group of a given name. Here’s how:

hub.Groups.AddAsync(connectionId, nameOfGroup);

Connected clients receive their data through a callback. This is only the most common technique. In ASP.NET Core SignalR, you can also use streaming.

Data Streaming

Probably the most interesting new aspect of SignalR is support for streaming. Streaming is similar to broadcasting, but it follows a slightly different model and is essentially a slightly different way of achieving the same broadcast-style communication. With SignalR streaming, the hub still needs to poll or listen for data in order to stream it back. In classic broadcast, the server tells a client method when new data is available.

In the new streaming model, the client subscribes to a new server object of type Channel and the server—the hub, actually—yields new items as they’re captured. At the moment, there’s nothing like a true stream that flows bytes toward all the connected clients as they become available, but this model can be supported in the future. Note that the Channel type has been introduced with preview2 and is not supported in earlier builds. In earlier builds, you must use observables instead, which require a reference to the System.Reactive.Linq NuGet package. The switch between observables and the new type Channel relates to the lack of primitives in IObservable for working with network backpressure (that is, telling the server to slow down when the client isn’t processing messages fast enough).

Figure 3 presents the code for the hub.

Figure 3 The Hub Class That Streams Back

public class ClockHub : Hub
  private static bool _clockRunning = false;
  public void Start()
    _clockRunning = true;
  public void Stop()
    _clockRunning = false;
  public ChannelReader<string> Tick()
  {    var channel = Channel.CreateUnbounded<string>();
    Task.Run(async() => {
        var time = DateTime.UtcNow.ToString("HH:mm:ss");
        await channel.Writer.WriteAsync(time);
        await Task.Delay(1000);
      channel.Writer.TryComplete();    });

The hub offers three methods to start, stop and operate the clock. A global variable controls the running status of the streaming, and start and stop methods set the control variable and notify back client methods as usual in a SignalR hub. The tricky part is the Tick method. The method name coincides with the name of the stream to which clients will subscribe. The method returns a channel object of a given type. In the example, the type is a simple string but it can be anything more sophisticated.

Every invocation, from client to server or server to client, consists of one party sending an invocation message, and the other party eventually responding with a completion message that carries a result or an error. In a SignalR streaming scenario, instead, the other party responds with multiple messages, each carrying a data item, before eventually concluding the communication with a completion message. As a result, the client ends up processing multiple items even before the completion message is received.

Scaling to Multiple Instances

SignalR keeps all connection IDs in memory, meaning that the moment the application scales to multiple instances, the broadcast (but also streaming, as discussed later) is compromised as each instance would only track a portion of all connected clients. To avoid that, SignalR supports a Redis-based cache that ensures that new connections are automatically shared between instances. To enable Redis, you need the SignalR.Redis package and a call to the AddRedis method in the ConfigureServices method of the startup class, like so:

        .AddRedis("connection string");

The option parameter serves the purpose of specifying the connection string to the running instance of Redis.

Wrapping Up

ASP.NET Core SignalR comes with two significant changes from the non-Core version. One is the lack of automatic reconnection, which has an impact on how connect/disconnect and online user count is handled programmatically. This means that now every application has to handle connection/disconnection logic and likely has to identify the difference between a user connecting for the first time and a user reconnecting due to an error. The other change is support for data streaming. Data streaming is based on channels and at the moment only supports specific data items instead of raw streams.

Finally, my exploration of SignalR lacks one more piece, which I’ll address in a future column: authenticated users and groups.

Dino Esposito has authored more than 20 books and 1,000 articles in his 25-year career. Author of “The Sabbatical Break,” a theatrical-style show, Esposito is busy writing software for a greener world as the digital strategist at BaxEnergy. Follow him on Twitter: @despos.

Thanks to the following Microsoft expert for reviewing this article: Andrew Stanton-Nurse

Discuss this article in the MSDN Magazine forum