SignalR

MassTransit offers a package which provides an easy option to get a SignalR Backplane up and running in with just a few lines of configuration. We won't go over the concept of a SignalR Backplane, more details can be found out about it here. This page is old, and references the .NET Framework SignalR, but the concepts of scale out are the same for the newer .NET Core SignalR.

.NET Framework SignalR (which MassTransit does not support) Backplane Options:

  • SQLServer
  • Redis
  • Azure Service Bus

.NET Core SignalR (which MassTransit WILL work for) Backplane Options:

  • Redis (official)
  • Azure SignalR Service (official)
  • MassTransit (unofficial)
    • RabbitMq
    • ActiveMq
    • Azure Service Bus

Quickstart

In your ASP.NET Core Startup.cs file add the following

public void ConfigureServices(IServiceCollection services)
{
    // other config...
    
    services.AddSignalR();

    // Other config perhaps...

    // creating the bus config
    services.AddMassTransit(x =>
    {
        // Add this for each Hub you have
        x.AddSignalRHub<ChatHub>(cfg => {/*Configure hub lifetime manager*/});

        x.UsingRabbitMq((context, cfg) =>
        {
            cfg.Host("localhost", "/", h =>
            {
                h.Username("guest");
                h.Password("guest");
            });
          
            // register consumer' and hub' endpoints
            cfg.ConfigureEndpoints(context);
        }));
    });
}

There you have it. All the consumers needed for the backplane are added to a temporary endpoint. ReceiveEndpoints without any queue name are considered Non Durable, and Auto Deleting.

Hub Endpoints

The core of communication contracts between the client and server are hubs. Depending on your application and complexity you might have a few hubs as a separation of concern for your application. The backplanes work through 5 types of events per hub.

So this translated well into MassTransit Events:

  • All<THub> - Invokes the method (with args) for each connection on the specified hub
    • The ExcludedConnectionIds property allows exclusion of specific connection ids
  • Connection<THub> - Invokes the method (with args) for the specific connection
    • The ConnectionId indicates which connection id to send the message to. If no active connection was found for this id, no exception will be thrown and the message will be completed without further processing.
  • Group<THub> - Invokes the method (with args) for all connections belonging to the specified group
    • The GroupName property indicates the name of the target group
    • The ExcludedConnectionIds property allows exclusion of specific connection ids
  • GroupManagement<THub> - Adds or removes a connection to the group (on a remote server)
  • User<THub> - Invokes the method (with args) for all connections belonging to the specific user id
    • The UserId property indicates the id of the user to be targeted. Note that although similar, this differs from Connection<THub> since the user id is generally a specific identifier given to a user, whereas the connection id is usually a random identifier assigned to each connection

All event types contain a property Messages which transports the payload as an IReadOnlyDictionary<string, byte[]>.

So each of these Messages has a corresponding consumer, and it will get a HubLifetimeManager<THub> through DI to perform the specific task. Messages sent through these endpoints will be published on your configured message broker, and once consumed, will be sent to your SignalR clients according to the configured message type.

In case an exception occurs while sending a message through the SignalR connection, an exception will be logged, but the message itself will still be marked as completed.

MassTransit's helper extension method will create an endpoint per consumer per hub, which follows the typical recommendation of one consumer per endpoint. Because of this, the number of endpoints can grow quickly if you have many hubs. It's best to also read some SignalR Limitations, to understand what can become potential bottlenecks with SignalR and your backplane. SignalR recommends re-thinking your strategy for very high throughput, real-time applications (video games).

Interop

The nice thing about using MassTransit as the back end is we can interact with the backplane by publishing the appropriate message (with hub).

I can't think of a scenario you would ever publish GroupManagement<THub>. Only All<THub>, Connection<THub>, Group<THub>, and User<THub> should be used.

To publish a message from a back end service (eg. console app, Topshelf):

await busControl.Publish<All<ChatHub>>(new
{
    Messages = protocols.ToProtocolDictionary("broadcastMessage", new object[] { "backend-process", "Hello" })
});

You are done!

The lifetime of the message looks like this:

  1. An All<ChatHub> message, including your payload will be published to your configured broker.
  2. All (or a select few, depending on how you configured MassTransit) consumers which registered .AddSignalRHub<ChatHub>(...) will receive the message.
  3. The message will be published through the SignalR connection.
  4. Your client will receive the message.

A slightly more complex example of how you would send a message to a specific group, but exclude specific connection ids would look like this:

await busControl.Publish<Group<ChatHub>>(new
{
    GroupName = "ServiceDeskEmployees",
    ExcludedConnectionIds = new [] { "11b9c749-69a2-4f3e-8a8b-968122156220", "1737778b-c836-4023-a255-51c2e4898c43" },
    Messages = protocols.ToProtocolDictionary("broadcastMessage", new object[] { "backend-process", "Hello" })
});

This example would send the message to a group called "ServiceDeskEmployees", but exclude the specified connection ids from receiving the message.

Complex Hubs

Your ASP.NET Core might have complex Hubs, with multiple interfaces injected.

public class ProductHub : Hub
{
    public ProductHub(
        IService1 service1,
        IService2 service2,
        ICache cache,
        IMapper mapper
    )
    {
        //...
    }

    // Hub Methods...
}

Your back end service might exist in a separate project and namespace, with no knowledge of the hubs or injected services. However, even if said service does not use SignalR, you might still want to publish messages which pass through the broker and end up being sent to your client.

Because MassTransit routes messages by namespace+message, I recommend to create a marker hub(s) within your back end service just for use of publishing. This saves you having to have all the hub(s) injected dependencies also within your back end service and still allows your service to publish namespace-compliant messages to be picked up by your SignalR integration.

namespace YourNamespace.Should.Match.The.Hubs
{
    public class ProductHub : Hub
    {
        // That's it, nothing more needed.
    }
}

Protocol Dictionary

SignalR supports multiple protocols for communicating with the Hub, the "serialized message" that is sent over the backplane is translated for each protocol method supported. The Extension method .ToProtocolDictionary(...) helps facilitate this translation into the protocol for communication.

Sample

We've included a sample ASP.NET Core project, and back end console application to show interoperability with the backplane. The only thing needed is RabbitMQ. I'd recommend using their docker image to spin up the broker.

Sample-SignalR

You can view the MassTransit Sample here. The sample was based off of Microsoft's chat sample, which is nearly identical to the tutorial here, except the only different is it's stripped down to the bare minimum (no razor Pages, bootstrap or JQuery libraries).

The other difference is the Javascript client callback method name is "ReceiveMessage" versus "broadcastMessage", but both samples are nearly the same. and the hub route is /chat versus /chatHub.

The other addition we added is in the Properties/launchSettings.json, which lets us start 2 profiles on different ports. Then helps simulate horizontal scaling.

Mvc Sample

You can simulate scaleout by running the two profiles.

> cd (your cloned Sample-SignalR)\src\SampleSignalR.Mvc
> dotnet run --launch-profile sample1
> dotnet run --launch-profile sample2

Now in two browser tabs, open up in each: http://localhost:5100http://localhost:5200

Then you can type a message in each, and see them show up in the other. The backplane works!!

Console Sample

If you have some back end services (console apps, or Mt Topshelf consumers), you might want to notify users/groups of things that have happened in real time. You can do this by running this console app.

> cd (your cloned Sample-SignalR)\src\SampleSignalR.Service
> dotnet run

An type in a message to broadcast to all connections. You will see the message in your browsers chat messages

Considerations