Persisting SignalR Connections Across Page Reloads

I recently had the need to keep a SignalR connection even if the page would reload. As far as I know, this cannot be done out of the box, either with hubs or persistent connections. I looked it up, but could find no solid solution, so here is my solution!

First, we need to create a “session id” that is to be stored at the browser side. Mind you, this is not an ASP.NET session, nor a SignalR connection id, it’s something that uniquely identifies a session. To maintain sessions we normally use cookies, but my solution uses instead HTML5 session storage. I had to generate a session id, and there were several solutions available, from pseudo-GUIDs, to the SignalR connection id, but I ultimately decided to use the timestamp; here is it:

function getSessionId()

{

    var sessionId = window.sessionStorage.sessionId;

    

    if (!sessionId)

    {

        sessionId = window.sessionStorage.sessionId = Date.now();

    }

    

    return sessionId;

}

As you can see, this function first checks to see if the session id was created, by inspecting the sessionStorage object, and, if not, sets it.

Next, we need to have SignalR pass this session id on every request to the server. For that, I used $.connection.hub.qs, the query string parameters object:

$.connection.hub.qs = { SessionId: getSessionId() };

$.connection.hub.start().done(function ()

{

    //connection started

});

Moving on to the server-side, I used a static collection to store, for each session id, each SignalR connection id associated with it – one for each page request. The reasoning is, each page reload generates a new SignalR connection id, but the session id is always kept:

public sealed class NotificationHub : Hub

{

    internal const string SessionId = "SessionId";

 

    public static readonly ConcurrentDictionary<string, HashSet<string>> sessions = new ConcurrentDictionary<string, HashSet<string>>();

 

    public static IEnumerable<string> GetAllConnectionIds(string connectionId)

    {

        foreach (var session in sessions)

        {

            if (session.Value.Contains(connectionId) == true)

            {

                return session.Value;

            }

        }

 

        return Enumerable.Empty<string>();

    }

 

    public override Task OnReconnected()

    {

        this.EnsureGroups();

 

        return base.OnReconnected();

    }

 

    public override Task OnConnected()

    {

        this.EnsureGroups();

 

        return base.OnConnected();

    }

 

    private void EnsureGroups()

    {

        var connectionIds = null as HashSet<string>;

        var sessionId = this.Context.QueryString[SessionId];

        var connectionId = this.Context.ConnectionId;

 

        if (sessions.TryGetValue(sessionId, out connectionIds) == false)

        {

            connectionIds = sessions[sessionId] = new HashSet<string>();

        }

 

        connectionIds.Add(connectionId);

    }

}

As you can see, both on OnConnected as in OnReconnected, I add the current connection id to the collection (ConcurrentDictionary<TKey, TValue> to allow multiple concurrent accesses) indexed by the session id that I sent in the SignalR query string. Then, I have a method that looks in the collection for all connection id entries that are siblings of a given connection id. If more than one exists, it means that the page has reloaded, otherwise, there will be a one-to-one match between connection ids and session ids.

The final step is to broadcast a message to all the sibling connection ids – a waste of time because only one is still possibly active, but since we have no way of knowing, it has to be this way:

[HttpGet]

[Route("notify/{connectionId}/{message}")]

public IHttpActionResult Notify(string connectionId, string message)

{

    var context = GlobalHost.ConnectionManager.GetHubContext<NotificationHub>();

    var connectionIds = NotificationHub.GetAllConnectionIds(connectionId).ToList();

 

    context.Clients.Clients(connectionIds).MessageReceived(message);

 

    return this.Ok();

}

This Web API action method will get the context for our hub (NotificationHub), look up all of the sibling connection ids for the passed one, and then broadcast a message to all clients identified by these connection ids. It’s a way to send messages from outside of a page into a hub’s clients

Problems with this approach:

  • All tabs will get the same session id, but that also happens with cookies;
  • Although unlikely, it may be possible for two clients to get the same session id, which I implemented as the current timestamp; an easy fix would be, for example, to use a pseudo-GUID, the server-side session id, or even the SignalR connection id;
  • If the page reloads several times, there will be several connection id entries for the same session id – which will be kept throughout all reloads; no easy way to get around this, except possibly using some cache with expiration mechanism.

And that’s it. Enjoy!