OrgnalR is a backplane for SignalR core, implemented through Orleans!
It allows your SignalR servers to scale out with all the capacity of Orleans grains.
This is an alternative to the Redis backplane, and SignalR.Orleans. This implementation does not use Orleans streams at all. This project was born out of issues with deadlocks that occured with Orleans streams, and since SignalR.Orleans uses them, issues with signalr messages not going through.
Orleans 7.0.0
introduced a large breaking change around serialization and other Orleans primitives. As of ^2.0.0
, OrgnalR only supports .net/Orleans 7.0.0
and up. If you need to use this package for an older release of Orleans and .net (including .netstandard), see the 1.X.X
releases.
OrgnalR comes in two packages, one for the Orleans Silo, and one for the SignalR application.
dotnet add package OrgnalR.SignalR
dotnet add package OrgnalR.OrleansSilo
OrgnalR can be configured via extension methods on both the Orleans client/silo builders, and the SignalR builder.
Somewhere in your Startup.cs
(or wherever you configure your SignalR server), you will need to add an extension method to the SignalR builder. The extension method lives in the OrgnalR.SignalR
namespace, so be sure to add a using for that namespace.
using OrgnalR.SignalR;
class Startup {
public void ConfigureServices(IServiceCollection services)
{
services.AddSignalR()
.UseOrgnalR();
}
}
Next, Orleans needs to know how to serialize your Hub Requests and Responses.
The easiest way to achieve this is to annotate your request/response classes with the GenerateSerializer
attribute, as you would with any of your normal grain models.
Example:
[GenerateSerializer]
public record SendMessageRequest(string ChatName, string SenderName, string Message);
// Usage in a hub
public class ChatHub : Hub<IChatClient>
{
public async Task SendMessage(SendMessageRequest request)
}
Please see the example directory for a fully working solution.
Wherever you configure your orleans Silo, you will need to configure OrgnalR's grains. This is again accomplished by an extension method, however there are two different modes. For development, it is easiest to use the AddOrgnalRWithMemoryGrainStorage
extension method, which registers the storage providers for the grains with memory storage. This is undesirable for production as if the silo dies, the information on connections in which groups is lost.
For production usage it is best to configure actual persistent storage for ORGNALR_USER_STORAGE
, ORGNALR_GROUP_STORAGE
, and MESSAGE_STORAGE_PROVIDER
, then use the AddOrgnalR
extension method.
Both of these methods are found in the OrgnalR.Silo
namespace.
var builder = new SiloHostBuilder()
/* Your other configuration options */
// Note here we use the memory storage option.
// This is good for quick development, but we should register proper storage for production
.AddOrgnalRWithMemoryGrainStorage()
var builder = new SiloHostBuilder()
/* Your other configuration options */
// Note here we specify the storage we will use for group and user membership
.ConfigureServices(services =>
{
services.AddSingletonNamedService<IGrainStorage, YourStorageProvider>(Extensions.USER_STORAGE_PROVIDER);
services.AddSingletonNamedService<IGrainStorage, YourStorageProvider>(Extensions.GROUP_STORAGE_PROVIDER);
services.AddSingletonNamedService<IGrainStorage, YourStorageProvider>(Extensions.MESSAGE_STORAGE_PROVIDER);
})
.AddOrgnalR()
And that's it! Your SignalR server will now use the OrgnalR backplane to send messages, and maintain groups / users.
Sometimes it is useful to send messages to clients from outside of the Hub. SignalR exposes an interface IHubContext<T>
for this mechanism inside of the server apps which expose SignalR hubs.
However, in the context of an orleans app, this requirement might still be necessary from the Silo host. To facilitate this, OrgnalR exposes a interface: IHubContextProvider
.
To send messages to connected clients in a hub, simply inject this interface into your grain (or service).
It exposes methods for getting clients by group/user/connectionID. You can then call SendAsync
to send them a message.
Note that SendAsync
is an extension method provided by the Microsoft.AspNetCore.SignalR
namespace.
Example:
class MyGrain : IMyGrain{
private readonly IHubContextProvider hubContextProvider;
constructor(IHubContextProvider hubContextProvider)
{
this.hubContextProvider = hubContextProvider;
}
async Task MyGrainMethod()
{
// Do stuff
// ...
// Send message to all connected clients in "MyHub"
await hubContextProvider
.GetHubContext<IMyHub>()
.Clients
.All // can also use Group, or User, or Connection
.SendAsync("MyClientMethod", new MyClientMethodRequest("Sent a message from a grain!"));
}
}
Alternatively, if your application has defined interfaces for strongly typed client methods, you can use the generic form:
interface IMyClient{
Task MyClientMethod(MyClientMethodRequest request);
}
class MyGrain : IMyGrain{
private readonly IHubContextProvider hubContextProvider;
constructor(IHubContextProvider hubContextProvider)
{
this.hubContextProvider = hubContextProvider;
}
async Task MyGrainMethod()
{
// Do stuff
// ...
// Send message to all connected clients in "MyHub"
await hubContextProvider
.GetHubContext<IMyHub, IMyClient>()
.Clients
.All // can also use Group, or User, or Connection
.MyClientMethod(new MyClientMethodRequest("Sent a message from a grain!"));
}
}
Examples can be found in the example directory The current example is a chat room which uses grains to store the messages, and OrgnalR as a SignalR backplane. React frontend.
Contributions are welcome! Simply fork the repository, and submit a PR. If you have an issue, feel free to submit an issue :)