Sending Messages to SignalR Hubs from the Outside
Introduction
You are by now probably familiarized with SignalR, Microsoft’s API for real-time web functionality. This is, in my opinion, one of the greatest products Microsoft has released in recent time.
Usually, people login to a site and enter some page which is connected to a SignalR hub. Then they can send and receive messages – not just text messages, mind you – to other users in the same hub. Also, the server can also take the initiative to send messages to all or a specified subset of users on its own, this is known as server push.
The normal flow is pretty straightforward, Microsoft has done a great job with the API, it’s clean and quite simple to use. And for the latter – the server taking the initiative – it’s also quite simple, just involves a little more work.
The Problem
The API for sending messages can be achieved from inside a hub – an instance of the Hub class – which is something that we don’t have if we are the server and we want to send a message to some user or group of users: the Hub instance is only instantiated in response to a client message.
The Solution
It is possible to acquire a hub’s context from outside of an actual Hub instance, by calling GlobalHost.ConnectionManager.GetHubContext<T>(). This API allows us to:
-
Broadcast messages to all connected clients (possibly excluding some);
-
Send messages to a specific client;
-
Send messages to a group of clients.
So, we have groups and clients, each is identified by a string. Client strings are called connection ids and group names are free-form, given by us. The problem with client strings is, we do not know how these map to actual users.
One way to achieve this mapping is by overriding the Hub’s OnConnected and OnDisconnected methods and managing the association there. Here’s an example:
1: public class MyHub : Hub
2: {
3: private static readonly IDictionary<String, ISet<String>> users = new ConcurrentDictionary<String, ISet<String>>();
4:
5: public static IEnumerable<String> GetUserConnections(String username)
6: {
7: ISet<String> connections;
8:
9: users.TryGetValue(username, out connections);
10:
11: return (connections ?? Enumerable.Empty<String>());
12: }
13:
14: private static void AddUser(String username, String connectionId)
15: {
16: ISet<String> connections;
17:
18: if (users.TryGetValue(username, out connections) == false)
19: {
20: connections = users[username] = new HashSet<String>();
21: }
22:
23: connections.Add(connectionId);
24: }
25:
26: private static void RemoveUser(String username, String connectionId)
27: {
28: users[username].Remove(connectionId);
29: }
30:
31: public override Task OnConnected()
32: {
33: AddUser(this.Context.Request.User.Identity.Name, this.Context.ConnectionId);
34: return (base.OnConnected());
35: }
36:
37: public override Task OnDisconnected()
38: {
39: RemoveUser(this.Context.Request.User.Identity.Name, this.Context.ConnectionId);
40: return (base.OnDisconnected());
41: }
42: }
As you can see, I am using a static field to store the mapping between a user and its possibly many connections – for example, multiple open browser tabs or even multiple browsers accessing the same page with the same login credentials. The user identity, as is normal in .NET, is obtained from the IPrincipal which in SignalR hubs case is stored in Context.Request.User. Of course, this property will only have a meaningful value if we enforce authentication.
Another way to go is by creating a group for each user that connects:
1: public class MyHub : Hub
2: {
3: public override Task OnConnected()
4: {
5: this.Groups.Add(this.Context.ConnectionId, this.Context.Request.User.Identity.Name);
6: return (base.OnConnected());
7: }
8:
9: public override Task OnDisconnected()
10: {
11: this.Groups.Remove(this.Context.ConnectionId, this.Context.Request.User.Identity.Name);
12: return (base.OnDisconnected());
13: }
14: }
In this case, we will have a one-to-one equivalence between users and groups. All connections belonging to the same user will fall in the same group.
So, if we want to send messages to a user from outside an instance of the Hub class, we can do something like this, for the first option – user mappings stored in a static field:
1: public void SendUserMessage(String username, String message)
2: {
3: var context = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
4:
5: foreach (String connectionId in HelloHub.GetUserConnections(username))
6: {
7: context.Clients.Client(connectionId).sendUserMessage(message);
8: }
9: }
And for using groups, its even simpler:
1: public void SendUserMessage(String username, String message)
2: {
3: var context = GlobalHost.ConnectionManager.GetHubContext<MyHub>();
4:
5: context.Clients.Group(username).sendUserMessage(message);
6: }
Using groups has the advantage that the IHubContext interface returned from GetHubContext has direct support for groups, no need to send messages to individual connections.
Of course, you can wrap both mapping options in a common API, perhaps exposed through IoC. One example of its interface might be:
1: public interface IUserToConnectionMappingService
2: {
3: //associate and dissociate connections to users
4:
5: void AddUserConnection(String username, String connectionId);
6:
7: void RemoveUserConnection(String username, String connectionId);
8: }
SignalR has built-in dependency resolution, by means of the static GlobalHost.DependencyResolver property:
1: //for using groups (in the Global class)
2: GlobalHost.DependencyResolver.Register(typeof(IUserToConnectionMappingService), () => new GroupsMappingService());
3:
4: //for using a static field (in the Global class)
5: GlobalHost.DependencyResolver.Register(typeof(IUserToConnectionMappingService), () => new StaticMappingService());
6:
7: //retrieving the current service (in the Hub class)
8: var mapping = GlobalHost.DependencyResolver.Resolve<IUserToConnectionMappingService>();
Now all you have to do is implement GroupsMappingService and StaticMappingService with the code I shown here and change SendUserMessage method to rely in the dependency resolver for the actual implementation.
Stay tuned for more SignalR posts!