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
- The
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.
- The
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
- The
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 fromConnection<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
- The
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:
- An
All<ChatHub>
message, including your payload will be published to your configured broker. - All (or a select few, depending on how you configured MassTransit) consumers which registered
.AddSignalRHub<ChatHub>(...)
will receive the message. - The message will be published through the SignalR connection.
- 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
- Sticky Sessions is required, unless you force Websockets only
- Also a good read
- Although this page is written for the old SignalR, the scale out concepts still apply.
- Having a single hub is fine, but only use multiple hubs for organization, not performance.