Mapping ASP.NET SignalR Connections to Real Application Users

One of the common questions about SignalR is how to broadcast a message to specific users and the mapping the SignalR connections to your real application users is the key component for this.
2013-01-01 17:32
Tugberk Ugurlu


SignalR; the incredible real-time web framework for .NET. You all probably heard of it, maybe played with it and certainly loved it. If you haven’t, why don’t you start by reading the SignalR docs and @davidfowl’s blog post on Microsoft ASP.NET SignalR? Yes, you heard me right: it’s now officially a Microsoft product, too (and you may see as a bad or good thing).

One of the common questions about SignalR is how to broadcast a message to specific users and the answer depends on what you are really trying to do. If you are working with Hubs, there is this notion of Groups which you can add connections to. Then, you can send messages to particular groups. It’s also very straight forward to work with Groups with the latest SignalR server API:

public class MyHub : Hub
{
    public Task Join()
    {
        return Groups.Add(Context.ConnectionId, "foo");
    }

    public Task Send(string message)
    {
        return Clients.Group("foo").addMessage(message);
    }
}

You also have a chance to exclude some connections within a group for that particular message. However, if you have more specific needs such as broadcasting a message to user x, Groups are not your best bet. There are couple of reasons:

  • SignalR is not aware of any of your business logic. SignalR knows about currently connected connections and their connection ids. That’s all.
  • Assume that you have some sort of authentication on your application (forms authentication, etc.). In this case, your user can have multiple connection ids by consuming the application with multiple ways. So, you cannot just assume that your user has only one connection id.

By considering these factors, mapping the SignalR connections to actual application users is the best way to solve this particular problem here. To demonstrate how we can actually solve this problem with code, I’ve put together a simple chat application and the source code is also available on GitHub.

image

This application obviously not a production ready application and the purpose here is to show how to achieve connection mapping. I didn’t even use a persistent data storage technology for this demo.

The scenarios I needed to the above sample are below ones:

  • A user can log in with his/her username and can access to the chat page. A user also can sign out whenever they want.
  • A user can see other connected users on the right hand side at the screen.
  • A user can send messages to all connected users.
  • A user can send private messages to a particular user.

To achieve the first goal, I have a very simple ASP.NET MVC controller:

public class AccountController : Controller {

    public ViewResult Login() {

        return View();
    }

    [HttpPost]
    [ActionName("Login")]
    public ActionResult PostLogin(LoginModel loginModel) {

        if (ModelState.IsValid) {

            FormsAuthentication.SetAuthCookie(loginModel.Name, true);
            return RedirectToAction("index", "home");
        }

        return View(loginModel);
    }

    [HttpPost]
    [ActionName("SignOut")]
    public ActionResult PostSignOut() {

        FormsAuthentication.SignOut();
        return RedirectToAction("index", "home");
    }
}

When you hit the home page as an unauthenticated user, you will get redirected to login page to log yourself in. As you can see from the PostLogin action method, everybody can authenticate themselves by simply entering their name which is obviously not what you would want in a real world application.

As I am hosting my SignalR application under the same process with my ASP.NET MVC application, the authenticated users will flow through the SignalR pipeline, too. So, I protected my Hub and its methods with the Microsoft.AspNet.SignalR.Hubs.AuthorizeAttribute.

[Authorize]
public class ChatHub : Hub { 

    //...
}

As we are done with the authorization and authentication pieces, we can now move on and implement our Hub. What I want to do first is to keep track of connected users with a static dictionary. Now, keep in mind again here that you would not want to use a static dictionary on a real world application, especially when you have a web farm scenario. You would want to keep track of the connected users with a persistent storage system such as MongoDB, RavenDB, SQL Server, etc. However, for our demo purposes, a static dictionary will just work fine.

public class User {

    public string Name { get; set; }
    public HashSet<string> ConnectionIds { get; set; }
}

[Authorize]
public class ChatHub : Hub {

    private static readonly ConcurrentDictionary<string, User> Users 
        = new ConcurrentDictionary<string, User>();
        
    // ...
}

Each user will have a name and associated connection ids. Now the question is how to add and remove values to this dictionary. SignalR raises three particular events on your hub: OnConnected, OnDisconnected, OnReconnected and the purposes of these events are very obvious.

During OnConnected event, we need to add the current connection id to the user’s connection id collection (we need to create the User object first if it doesn’t exist inside the dictionary). We also want to broadcast this information to all clients so that they can update their connected users list. Here is how I implemented the OnConnected method:

[Authorize]
public class ChatHub : Hub {

    private static readonly ConcurrentDictionary<string, User> Users 
        = new ConcurrentDictionary<string, User>();
        
    public override Task OnConnected() {

        string userName = Context.User.Identity.Name;
        string connectionId = Context.ConnectionId;

        var user = Users.GetOrAdd(userName, _ => new User {
            Name = userName,
            ConnectionIds = new HashSet<string>()
        });

        lock (user.ConnectionIds) {

            user.ConnectionIds.Add(connectionId);
            
            // TODO: Broadcast the connected user
        }

        return base.OnConnected();
    }
}

First of all, we have gathered the currently authenticated user name and connected user’s connection id. Then, we look inside the dictionary to get the user based on the user name. If it doesn’t exist inside the dictionary, we create one and set it to the local variable named user. Lastly, we add the connection id and updated the dictionary.

Notice that we have a TODO comment at the end telling that we need to broadcast the connected user’s name. Obviously, we don’t want to broadcast this information to the caller itself. However, we still have two options here and which one you would choose may depend on your case. As the user might have multiple connections, broadcasting this information over Clients.Others API is not a way to follow. Instead, we can use Clients.AllExcept method which takes a list of connection ids as parameter to exclude. So, we can pass the all connection ids of the user and we are good to go.

public override Task OnConnected() {

    // Lines omitted for brevity
    
    Clients.AllExcept(user.ConnectionIds.ToArray()).userConnected(userName);

    return base.OnConnected();
}

This is a fine approach if we want to broadcast each connection of the user to every client other than the user itself. However, we may only want to broadcast the first connection. Doing so is very straight forward, too. We just need to inspect the count of the connection ids and if it equals to one, we can broadcast the information. This approach is the one that I ended up taking for this demo.

public override Task OnConnected() {

    // Lines omitted for brevity

    lock (user.ConnectionIds) {

        // Lines omitted for brevity
        
        if (user.ConnectionIds.Count == 1) {

            Clients.Others.userConnected(userName);
        }
    }

    return base.OnConnected();
}

When the disconnect event is fired, OnDisconnected method will be called and we need to remove the current connection id from the users dictionary. Similar to what we have done inside the OnConnected method, we need to handle the fact that user can have multiple connections and if there is no connection left, we want to remove the user from Users dictionary completely. As we did when a user connection arrives, we need to broadcast the disconnected users, too and we have the same two options here as well. I added both to the below code and commented out the one that we don’t need for our demo.

[Authorize]
public class ChatHub : Hub {

    private static readonly ConcurrentDictionary<string, User> Users 
        = new ConcurrentDictionary<string, User>();

    public override Task OnDisconnected() {

        string userName = Context.User.Identity.Name;
        string connectionId = Context.ConnectionId;
        
        User user;
        Users.TryGetValue(userName, out user);
        
        if (user != null) {

            lock (user.ConnectionIds) {

                user.ConnectionIds.RemoveWhere(cid => cid.Equals(connectionId));

                if (!user.ConnectionIds.Any()) {

                    User removedUser;
                    Users.TryRemove(userName, out removedUser);

                    // You might want to only broadcast this info if this 
                    // is the last connection of the user and the user actual is 
                    // now disconnected from all connections.
                    Clients.Others.userDisconnected(userName);
                }
            }
        }

        return base.OnDisconnected();
    }
}

When the OnReconnected method is invoked, we don’t need to perform any special logic here as the connection id will be the same. With these implementations, we are now keeping track of the connected users and we have mapped the connections to real application users.

Going back to our scenarios list above, we have the 4th requirement: a user sending private messages to a particular user. This is where we actually need the connection mapping functionality. As an high level explanation, the client will send the name of the user that s/he wants to send the message to privately. So, server needs to make sure that it is only sending the message to the designated user. I am not going to go through all the client code (as you can check them out from the source code and they are not that much related to the topic here) but the piece of JavaScript code that actually decides whether to send a public or private message is as below:

$sendBtn.click(function (e) {

    var msgValue = $msgTxt.val();
    if (msgValue !== null && msgValue.length > 0) {

        if (viewModel.isInPrivateChat()) {

            chatHub.server.send(msgValue, viewModel.privateChatUser()).fail(function (err) {
                console.log('Send method failed: ' + err);
            });
        }
        else {
            chatHub.server.send(msgValue).fail(function (err) {
                console.log('Send method failed: ' + err);
            });
        }
    }
    e.preventDefault();
});

The above code inspects the KnockoutJS view model to see if the sender is at the private chat mode. If s/he is, it invokes the send hub method on the sever with two parameters which means that this will be a private message. If the sender is not at the private chat mode, we will just invoke the send hub method by passing only one parameter for the message. Let’s first look at Send Hub method that takes one parameter:

public void Send(string message) {

    string sender = Context.User.Identity.Name;

    Clients.All.received(new { 
        sender = sender, 
        message = message, 
        isPrivate = false 
    });
}

Inside the send method above, we first retrieved the sender's name through the authenticated user principal. Then, we are broadcasting the message to all clients with a few more information such as the sender name and the privacy state of the message. Let’s now look at the second Send method inside the Hub whose job is to send private messages:

public void Send(string message, string to) {

    User receiver;
    if (Users.TryGetValue(to, out receiver)) {

        User sender = GetUser(Context.User.Identity.Name);

        IEnumerable<string> allReceivers;
        lock (receiver.ConnectionIds) {
            lock (sender.ConnectionIds) {

                allReceivers = receiver.ConnectionIds.Concat(
                    sender.ConnectionIds);
            }
        }

        foreach (var cid in allReceivers) {
        
            Clients.Client(cid).received(new { 
                sender = sender.Name, 
                message = message, 
                isPrivate = true 
            });
        }
    }
}

private User GetUser(string username) {

    User user;
    Users.TryGetValue(username, out user);

    return user;
}

Here, we are first trying to get the receiver based on the to parameter that we have received. If we find one, we are also retrieving the sender based on the his/her name. Now, we have the sender and the receiver in our hands. What we want is to broadcast this message to the receiver and the sender. So, we are putting the sender’s and the receiver’s connection ids together first. Finally, we are looping through that connection ids list to send the message to each connection by using the Clients.Client method which takes the connection id as a parameter.

When we try this out, we should see it working as below:

image

Grab the solution and try it yourself, too. I hope this post helped you to solve your problem Smile

References



Comments

MrOnosa
by MrOnosa on Wednesday, Jan 02 2013 22:22:55 +02:00

What is the purpose of updating user to user in the else branch in the OnDisconnected method?

             if (!user.ConnectionIds.Any()) {

                User removedUser;
                Users.TryRemove(userName, out removedUser);

                // You might want to only broadcast this info if this 
                // is the last connection of the user and the user actual is 
                // now disconnected from all connections.
                Clients.Others.userDisconnected(userName);
            }
            else {

                Users.AddOrUpdate(userName, user, (n, u) => user);
            }

 

Tugberk
by Tugberk on Wednesday, Jan 02 2013 22:32:48 +02:00

@MrOnosa

None. I have updated the code a lot based on @davidfowl's suggestions. I'll update the post soon.

MrOnosa
by MrOnosa on Thursday, Jan 03 2013 13:58:37 +02:00

Oh cool. I didn't even think to look at the latest code. I'll check that out. Thank you.

Tugberk
by Tugberk on Thursday, Jan 03 2013 14:15:04 +02:00

@MrOnosa

I updated the post, too.

Emre
by Emre on Monday, Feb 18 2013 17:48:49 +02:00

One of the best articles that i read about SignalR. I tried too much to find about mapping on SignalR and this is the answer now.Thanks.

Tugberk
by Tugberk on Thursday, Feb 21 2013 14:36:29 +02:00

@Emre

Glad that it helped ;)

Sparhawk
by Sparhawk on Sunday, Feb 24 2013 21:44:41 +02:00

Good article!

Let's assume that you store the connections in the database as you suggest. Let's further assume that your server has a restart because you are doing an update or similar. In those cases the Disconnect-Event is not firing and you'll keep the connections in the database.

Do you know any solution for that? I tried to go through all connections and find out if they are stale but without results.

Tugberk
by Tugberk on Friday, Mar 01 2013 10:39:22 +02:00

@Sparhawk

Nice question which I also wondered about before and you gave me a chance to write about it. 

There is no built-in way to compare the connected connection ids against the ones stored inside your database. What you can do (got the idea from David Fowler) is clear out the table, where you store your connection ids, on your application start up where you know for sure that there is no open connections.

However, if you are on a web farm, it may cause you to lose live connection ids if you clear out the connection id list in an instance start up. One approach to solve this problem: storing the connection ids with relation to an instance specific token (server name, etc.) so that you only clear out the related ones in that instance start up.

omab
by omab on Wednesday, Apr 10 2013 16:18:02 +03:00

I added message time to your code if someone is interested to use it, changes are:
in chat.js:

change Message function to:

function Message(from, msg, isPrivate) {
        this.from = ko.observable(from);
        this.message = ko.observable(msg);
        this.isPrivate = ko.observable(isPrivate);

        var d = new Date();
        var month = d.getMonth() + 1;
        var day = d.getDate();
        var time = d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds();
        var now = d.getFullYear() + '/' +
            (month < 10 ? '0' : '') + month + '/' +
            (day < 10 ? '0' : '') + day + ' | ' + time;

        this.date = now;
    }

and change viewModel.messages.push(new Message(message.sender, message.message, message.isPrivate));

to be:
viewModel.messages.push(new Message(message.sender, message.message, message.isPrivate, message.date));

in Index.cshtml : replace the ul to be:
<ul id="messages" data-bind="foreach: messages">
     <li>
         <div><span class="label label-important" data-bind="visible: isPrivate">Private</span></div>               
         <span data-bind="text: date" class="pull-right date"></span>
         <div style="margin-left:10px">
                 <strong><span data-bind="text: from"></span>: </strong>
                 <span data-bind="text: message"></span>
         </div>
     </li>
</ul>

 

add css class "date" to style tag:
.date {color:#6d6969; font-size:8pt;

Ilija Injac
by Ilija Injac on Saturday, May 18 2013 12:19:04 +03:00

You did a great job here! You blog-post helped me to understand the inner guts of a Hub. I think it is really time to have a library for different scenarios with specific Hub-implementations, or at least some very good samples like yours. Thank you.

Saša Tančev
by Saša Tančev on Wednesday, May 29 2013 18:46:54 +03:00

Hi,

I have problem with running MultiLayerSignalRSample sample and I am getting error:

Introducing FOREIGN KEY constraint 'FK_dbo.PrivateChatMessages_dbo.Users_ReceiverId' on table 'PrivateChatMessages' may cause cycles or multiple cascade paths.
Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint. See previous errors.

Thanks

wdc
by wdc on Monday, Jul 08 2013 07:25:46 +03:00

Bloody fantastic work, man!  I was looking into how to do this kind of thing and your article gives me great head start.  Thank you much!

Deepak Aggarwal
by Deepak Aggarwal on Sunday, Jul 28 2013 15:44:29 +03:00

This is an  exceptional article on SignalR. It burst those query bubbles which start flying once you start learning SignalR. Great work!

Leonardo Lima
by Leonardo Lima on Tuesday, Aug 06 2013 18:23:33 +03:00

Good article! But I cannot persist the changes on database inside the OnDisconnected method, because there is no HttpContext. So, the dependency resolver (Unity) is creating a new instance of my repository.

Obs.: I'm not implementing a chat, it's a notification system, so the hubs are being used in every page request.

Can anyone help to figure out the problem? Thanks in advance.

Mirza Salm
by Mirza Salm on Sunday, Sep 01 2013 15:05:12 +03:00

Hi,

I want to use signalR in asp.net 4.0 / web forms application. While I was trying to test my implimentation. I found that while we navigate from one .aspx page to another. the user gets disconnected and to re-connect the user. I have to start the connection on the second page as well. 

Is there any way to maintain a connection once user has loggeed in even he navigates on multiple pages. The connection should not be closed? or any way to get the session variables in my hub class?

 

Regards,

Salman

Saurabh Sashank
by Saurabh Sashank on Saturday, Sep 14 2013 08:10:36 +03:00

Hi,

This post was really awesome, i was trying similar kind of the application since 2 weeks i need your help in one of the functionality i want to add in my application. 

I am showing the list of all the online users(Connected), i can broadcast message to all the connected user but now i want to create different groups, suppose i have 10 connected user i want to create two groups of 5 user each and then send them message.

 

I am not using any authentication in my app. User simply Enters his name and he is connected.

 

Thanks & Regards

Saurabh Sashank

Dan Jarvis
by Dan Jarvis on Tuesday, Sep 17 2013 17:40:17 +03:00

Hi.

I have the same issue as Leonardo Lima in that in OnDisconnect() I have no HttpContext.  I persist things like the UserId and other things in a secure cookie much in the same way as Fedauth, but outside of it.  I cannot get to the UserId in OnDisconnect() so I do not know which SignalR groups to remove the ClientId from.  I guess I can try from each one.  Is there a RemoveFromAllGroups() call perhaps?

babita
by babita on Wednesday, Nov 20 2013 16:16:21 +02:00

i m curntly doin  a prjct in asp.net i have gridview control displaying records from database ,any changes in database the gridview should reflect how to do it with signalR ....

slots
by slots on Sunday, Dec 22 2013 05:41:33 +02:00
Thanky Thanky for all this good information!
Nir
by Nir on Sunday, Jan 19 2014 07:29:28 +02:00

Thanks for the useful post!

It seems that there is a problem with the Ondisconnected method. In some of the browsers this method is not getting fired...

I was thinking about a background method which every 1 minute checkes against the db for valid connections.

Is there a way to get all valid connection in the Hub class?

MANPREET
by MANPREET on Thursday, Feb 27 2014 07:33:16 +00:00
what is the code for view section..?
MANPREET
by MANPREET on Thursday, Feb 27 2014 07:40:23 +00:00
what is the code for view section..?
Bhanu
by Bhanu on Tuesday, Jul 15 2014 21:51:23 +00:00
Hi .. I am planning to implement the chat in my website... I need this chat to be visible across all the pages in the website, so I implemented the chat functionality in the master page... The problem I face is whenever a user starts chatting with someone , and move to the next page, the chat box disappears as the masterpage is loaded again when we switch to the other page. So user needs to open the chat window again... this is a bit frustrating... Could someone help me in solving this issue please... Thanks in advance...

New Comment