The Pub-Sub pattern is a great way to keep interested parties up-to-date on changes in state.  For web applications, sometimes the thing you want to track state on lives on the server, and the interested parties live on the client.  Using SignalR, you can easily propagate events out to interested clients without spamming uninterested clients.  Here’s how to do it.

[more]

Most of the examples you see with SignalR show the server calling client-side methods, and the client calling server-side methods on the hub.  Guess what: the actual mechanism underneath is already pretty-much just pub-sub.  You just have to get down to it.  So, the first thing you need to do is bypass this top-level abstraction. 

Let’s go ahead and create our server-side code first, then we’ll dive in to the client-side code. 

Here’s our SignalR hub:

public class SignalrEventHub : Hub
{
    
}

Yep, that’s it!  Our hub doesn’t need any methods, because our client isn’t going to actually call anything on it (yet).  It just exists to proxy messages back and forth between the client and the server.

Pushing an arbitrary message out through the hub, however, is a bit more complicated.  You need to get your hub, then cast it back to an instance of IClientProxy, at which point you can use the Invoke method to call client-side methods (which really just pushes down a message to the affected clients).  Here’s the wrapper I made:

public class EventHub
{
    public void PublishMessage(string topic, string message)
    {
        var hub = GlobalHost.ConnectionManager.GetHubContext<SignalrEventHub>();
        ((IClientProxy) hub.Clients.All).Invoke(topic, message).Wait();
    }

    public void PublishMessage<T>(string topic, T message)
    {
        var jsonValue = JsonConvert.SerializeObject(message, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });

        PublishMessage(topic, jsonValue);
    }
}

The hub takes in a topic and a message (either as a raw string, or as a complex object that will be serialized to JSON), then calls Invoke on the proxy, passing along the topic and the message.  On the client-side, SignalR will now look for a handler on that topic, and invoke the associated callback.

Now let’s look at the client-side code…

First, we need to grab an instance of our hub and start it…

client = $.connection.signalrEventHub;

//SUPER IMPORTANT!!  
client.on('_unused_', function (data) {
    console.log('Called unused!!', data);
});

$.connection.hub.start();

The client’s ‘on’ method is what makes it possible to subscribe to arbitrary server-side events.  When we register a callback like this, SignalR will invoke it for us anytime the server calls a method with that name.  Or, in the case of our EventHub above, whenever we ‘Invoke’ a topic. 

As you may have noticed, there’s one teeny tiny thing in there that’s very important: you must have ONE active subscription before you start the hub, otherwise *nothing* will come down from the server.   The code above just subscribes a dummy callback to a dummy event.  It should never actually be invoked, but just having the subscription defined is enough to get things working correctly.

Anyway, now that we have our hub, we can start subscribing to topics:

client.on('some.server-side.event', function (data) {
    console.log('Some event happened!', data);
});

On the server-side, when we do this…

var hub = new EventHub();
hub.Publish("some.server-side.event", someData);

Our client-side callback will be executed, and we’ll see “Some event happened!” (and our data) on the client-side!

There’s one (big) problem with this approach though: every single client is going to get every single notification from the server, even ones that the client hasn’t subscribed to.  The clients that aren’t subscribed will ignore the message, but still, that could be a lot of wasted bandwidth. To work around this little problem, we can let the server know when we subscribe from the client, then create a SingalR group for each topic on the server.

On the client-side, we call the server-side subscribe method (which we’re about to add)…

client.invoke('subscribe', 'some.server-side.event');
client.on('some.server-side.event', function (data) {
    console.log('Some event happened!', data);
});

And on the server-side, we add the corresponding implementation of Subscribe (and Unsubscribe for good measure)…

public class SignalrEventHub : Hub
{
        public void Subscribe(string topic)
        {
            Groups.Add(Context.ConnectionId, topic);
        }
        public void Unsubscribe(string topic)
        {
            Groups.Remove(Context.ConnectionId, topic);
        }
}

Now we can update our EventHub wrapper to only send out topics to the members of the topic group:

public class EventHub
{
    public void PublishMessage(string topic, string message)
    {
        var hub = GlobalHost.ConnectionManager.GetHubContext<SignalREventHub>();
        //Notice: now we're sending to a specified group, not all clients!
        ((IClientProxy) hub.Clients.Group(topic)).Invoke(topic, message).Wait();
    }

    public void PublishMessage<T>(string topic, T message)
    {
        var jsonValue = JsonConvert.SerializeObject(message, new JsonSerializerSettings
        {
            ContractResolver = new CamelCasePropertyNamesContractResolver()
        });

        PublishMessage(topic, jsonValue);
    }
}

Now only clients that have subscribed to a topic will be notified.  If no one is subscribed, SignalR won’t broadcast out anything.

It’s worth noting that things like WebSTOMP can also do pub sub from the client-side.  And if you hook it up to something like RabbitMQ, you can achieve this same sort of server-client (and client-server) messaging, and arguably with less hoop-jumping.   So why would you use SignalR instead?  Because SignalR does a lot of nice things for you.  It handles disconnects much more gracefully than the popular stomp.js client, which puts the burden on you, the consumer, to handle.  And if the client can’t make a websocket connection for some reason (such as an overly-restrictive corporate firewall!), SignalR can fall back to long-polling instead!