Skip to content

The search box in the website knows all the secrets—try it!

For any queries, join our Discord Channel to reach us faster.

JasperFx Logo

JasperFx provides formal support for Wolverine and other JasperFx libraries. Please check our Support Plans for more details.

Using SignalR 5.0

INFO

The SignalR transport has been requested several times, but finally got built specifically for the forthcoming "CritterWatch" product that will be used to monitor and manage Wolverine applications. In other words, the Wolverine team has heavily dog-fooded this feature.

TIP

Much of the sample code is taken from a runnable sample application in the Wolverine codebase called WolverineChat.

The SignalR library from Microsoft isn't hard to use from Wolverine for simplistic WebSockets or Server Side Events usage , but what if you want a server side application to exchange any number of different messages between a browser (or other WebSocket client because that's actually possible) and your server side code in a systematic way? To that end, Wolverine now supports a first class messaging transport for SignalR. To get started, just add a Nuget reference to the WolverineFx.SignalR library:

bash
dotnet add package WolverineFx.SignalR

Configuring the Server

TIP

Wolverine.SignalR does not require any usage of Wolverine.HTTP, but these two libraries can certainly be used in the same application as well.

The Wolverine.SignalR library sets up a single SignalR Hub type in your system (WolverineHub) that will be used to both send and receive messages from the browser. To set up both the SignalR transport and the necessary SignalR services in your DI container, use this syntax in the Program file of your web application:

cs
builder.UseWolverine(opts =>
{
    // This is the only single line of code necessary
    // to wire SignalR services into Wolverine itself
    // This does also call IServiceCollection.AddSignalR()
    // to register DI services for SignalR as well
    opts.UseSignalR(o =>
    {
        // Optionally configure the SignalR HubOptions
        // for the WolverineHub
        o.ClientTimeoutInterval = 10.Seconds();
    });
    
    // Using explicit routing to send specific
    // messages to SignalR
    opts.Publish(x =>
    {
        // WolverineChatWebSocketMessage is a marker interface
        // for messages within this sample application that
        // is simply a convenience for message routing
        x.MessagesImplementing<WolverineChatWebSocketMessage>();
        x.ToSignalR();
    });
});

snippet source | anchor

That handles the Wolverine configuration and the SignalR service registrations, but you will also need to map an HTTP route for the SignalR hub with this Wolverine.SignalR helper:

cs
var app = builder.Build();

app.UseRouting();

app.UseAuthorization();

app.MapStaticAssets();
app.MapRazorPages()
    .WithStaticAssets();

// This line puts the SignalR hub for Wolverine at the 
// designated route for your clients
app.MapWolverineSignalRHub("/api/messages");

return await app.RunJasperFxCommands(args);

snippet source | anchor

Messages and Serialization

For the message routing above, you'll notice that I utilized a marker interface just to facilitate message routing like this:

cs
// Marker interface for the sample application just to facilitate
// message routing
public interface WolverineChatWebSocketMessage : WebSocketMessage;

snippet source | anchor

The Wolverine WebSocketMessage marker interface does have a little bit of impact in that:

  1. It implements the IMessage interface that's just a helper for Wolverine to discover message types in your application upfront for diagnostics or upfront resource creation
  2. By marking your message types as WebSocketMessage, it changes Wolverine's message type name rules to using a Kebab-cased version of the message type name

For example, these three message types:

cs
public record ChatMessage(string User, string Text) : WolverineChatWebSocketMessage;
public record ResponseMessage(string User, string Text) : WolverineChatWebSocketMessage;

public record Ping(int Number) : WolverineChatWebSocketMessage;

snippet source | anchor

will result in these message type names according to Wolverine:

.NET TypeWolverine Message Type Name
ChatMessage"chat_message"
ResponseMessage"response_message"
Ping"ping"

That message type name is important because the Wolverine SignalR transport uses and expects a very light CloudEvents wrapper around the raw message being sent to the client and received from the browser. Here's an example of the JSON payload for the ChatMessage message:

json
{
  "type": "chat_message",
  "data": {
    "user": "Hank",
    "text": "Hey"
  }
}

You can always preview the message type name by using the dotnet run -- describe command and finding the "Message Routing" table in that output, which should look like this from the sample application:

text
                                        Message Routing                                         
┌───────────────────────────────┬────────────────────┬──────────────────────┬──────────────────┐
│ .NET Type                     │ Message Type Alias │ Destination          │ Content Type     │
├───────────────────────────────┼────────────────────┼──────────────────────┼──────────────────┤
│ WolverineChat.ChatMessage     │ chat_message       │ signalr://wolverine/ │ application/json │
│ WolverineChat.Ping            │ ping               │ signalr://wolverine/ │ application/json │
│ WolverineChat.ResponseMessage │ response_message   │ signalr://wolverine/ │ application/json │
└───────────────────────────────┴────────────────────┴──────────────────────┴──────────────────┘

The only elements that are mandatory are the type node that should be the Wolverine message type name and data that is the actual message serialized by JSON. Wolverine will send the full CloudEvents envelope structure because it's reusing the envelope mapping from our CloudEvents interoperability, but the browser code only needs to send type and data.

The actual JSON serialization in the SignalR transport is isolated from the rest of Wolverine and uses this default System.Text.Json configuration:

cs
JsonOptions = new(JsonSerializerOptions.Web) { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
JsonOptions.Converters.Add(new JsonStringEnumConverter());

snippet source | anchor

But of course, if you needed to override the JSON serialization for whatever reason, you can just push in a different JsonSerializerOptions like this:

cs
var builder = WebApplication.CreateBuilder();

builder.UseWolverine(opts =>
{
    // Just showing you how to override the JSON serialization
    opts.UseSignalR().OverrideJson(new JsonSerializerOptions
    {
        IgnoreReadOnlyProperties = false
    });
});

snippet source | anchor

Interacting with the Server from the Browser

It's not mandatory, but in developing and dogfooding the Wolverine.SignalR transport, we've found it helpful to use the actual signalr Javascript library and our sample SignalR application uses that library for the browser to server communication.

js
"use strict";

// Connect to the server endpoint
var connection = new signalR.HubConnectionBuilder().withUrl("/api/messages").build();

//Disable the send button until connection is established.
document.getElementById("sendButton").disabled = true;

// Receiving messages from the server
connection.on("ReceiveMessage", function (json) {
   // Note that you will need to deserialize the raw JSON
   // string
   const message = JSON.parse(json);

   // The client code will need to effectively do a logical
   // switch on the message.type. The "real" message is 
   // the data element
   if (message.type == 'ping'){
      console.log("Got ping " + message.data.number);
   }
   else{
      const li = document.createElement("li");
      document.getElementById("messagesList").appendChild(li);
      li.textContent = `${message.data.user} says ${message.data.text}`;
   }
});

connection.start().then(function () {
   document.getElementById("sendButton").disabled = false;
}).catch(function (err) {
   return console.error(err.toString());
});

document.getElementById("sendButton").addEventListener("click", function (event) {
   const user = document.getElementById("userInput").value;
   const text = document.getElementById("messageInput").value;

   // Remember that we need to wrap the raw message in this slim
   // CloudEvents wrapper
   const message = {type: 'chat_message', data: {'text': text, 'user': user}};

   // The WolverineHub method to call is ReceiveMessage with a single argument
   // for the raw JSON
   connection.invoke("ReceiveMessage", JSON.stringify(message)).catch(function (err) {
      return console.error(err.toString());
   });
   event.preventDefault();
});

Note that the method ReceiveMessage is hard coded into the WolverineHub service.

Our vision for this usage is that you probably integrate directly with a client side state tracking tool like Pinia (how we're using the SignalR transport to build "CritterWatch").

Sending Messages to SignalR

For the most part, sending a message to SignalR is just like sending messages with any other transport like this sample:

cs
public class Pinging : BackgroundService
{
    private readonly IWolverineRuntime _runtime;

    public Pinging(IWolverineRuntime runtime)
    {
        _runtime = runtime;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        var number = 0;
        while (!stoppingToken.IsCancellationRequested)
        {
            await Task.Delay(1.Seconds(), stoppingToken);
            
            // This is being published to all connected SignalR
            // applications
            await new MessageBus(_runtime).PublishAsync(new Ping(++number));
        }
    }
}

snippet source | anchor

The call above will occasionally send a Ping message to all connected clients. But of course, you'll frequently want to more selectively send messages to reply to the current connection or maybe to a specific group.

If you are handling a message that originated from SignalR, you can send a response back to the originating connection like this:

cs
public record RequestSum(int X, int Y) : WebSocketMessage;
public record SumAnswer(int Value) : WebSocketMessage;

public static class RequestSumHandler
{
    public static ResponseToCallingWebSocket<SumAnswer> Handle(RequestSum message)
    {
        return new SumAnswer(message.X + message.Y)
            
            // This extension method will wrap the raw message
            // with some helpers that will 
            .RespondToCallingWebSocket();
    }
}

snippet source | anchor

In the next section we'll learn a bit more about working with SignalR groups.

SignalR Groups

One of the powerful features of SignalR is being able to work with groups of connections. The SignalR transport currently has some simple support for managing and publishing to groups. Let's say you have these web socket messages in your system:

cs
public record EnrollMe(string GroupName) : WebSocketMessage;

public record KickMeOut(string GroupName) : WebSocketMessage;

public record BroadCastToGroup(string GroupName, string Message) : WebSocketMessage;

snippet source | anchor

The following code is a set of simplistic message handlers that handle these messages with some SignalR connection group mechanics:

cs
// Declaring that you need the connection that originated
// this message to be added to the named SignalR client group
public static AddConnectionToGroup Handle(EnrollMe msg) 
    => new(msg.GroupName);

// Declaring that you need the connection that originated this
// message to be removed from the named SignalR client group
public static RemoveConnectionToGroup Handle(KickMeOut msg) 
    => new(msg.GroupName);

// The message wrapper here sends the raw message to
// the named SignalR client group
public static SignalRMessage<Information> Handle(BroadCastToGroup msg) 
    => new Information(msg.Message)
        // This extension method wraps the "real" message 
        // with an envelope that routes this original message
        // to the named group
        .ToWebSocketGroup(msg.GroupName);

snippet source | anchor

In the code above:

  • AddConnectionToGroup and RemoveConnectionToGroup are both examples of Wolverine "side effects" that are specific to adding or removing the current SignalR connection (whichever connection originated the message and where the SignalR transport received the message)
  • ToWebSocketGroup(group name) is an extension method in Wolverine.SignalR that restricts the message being sent to SignalR to only being sent to connections in that named group

SignalR Client Transport

TIP

If you want to use the .NET SignalR Client for test automation, just know that you will need to bootstrap the service that actually hosts SignalR with the full stack including Kestrel. WebApplicationFactory will not be suitable for this type of integration testing through SignalR.

Wolverine.SignalR is actually two transports in one library! There is also a full fledged messaging transport built around the .NET SignalR client that we've used extensively for test automation, but could technically be used as a "real" messaging transport. The SignalR Client transport was built specifically to enable end to end testing against a Wolverine server that hosts SignalR itself. The SignalR Client transport will use the same CloudEvents mechanism to send and receive messages from the main Wolverine SignalR transport and is 100% compatible.

If you wanted to use the SignalR client as a "real" messaging transport, you could do that like this sample:

cs
var builder = Host.CreateApplicationBuilder();
builder.UseWolverine(opts =>
{
    // this would need to be an absolute Url to where SignalR is
    // hosted on your application and include the exact route where
    // the WolverineHub is listening
    var url = builder.Configuration.GetValue<string>("signalr.url");
    opts.UseClientToSignalR(url);

    // Setting this up to publish any messages implementing
    // the WebSocketMessage marker interface with the SignalR
    // client
    opts.Publish(x =>
    {
        x.MessagesImplementing<WebSocketMessage>();
        x.ToSignalRWithClient(url);
    });
});

snippet source | anchor

Or a little more simply, if you are just using this for test automation, you would need to give it the port number where your SignalR hosting service is running on the local computer:

cs
// Ostensibly, *something* in your test harness would 
// be telling you the port number of the real application
int port = 5555;

using var clientHost = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        // Just so you know it's possible, you can override
        // the relative url of the SignalR WolverineHub route
        // in the hosting application
        opts.UseClientToSignalR(port, "/api/messages");

        // Setting this up to publish any messages implementing
        // the WebSocketMessage marker interface with the SignalR
        // client
        opts.Publish(x =>
        {
            x.MessagesImplementing<WebSocketMessage>();
            x.ToSignalRWithClient(port);
        });
    }).StartAsync();

snippet source | anchor

To make this a little more concrete, here's a little bit of the test harness setup we used to test the Wolverine.SignalR transport:

cs
public abstract class WebSocketTestContext : IAsyncLifetime
{
    protected WebApplication theWebApp;
    private readonly int Port = PortFinder.GetAvailablePort();
    protected readonly Uri clientUri;

    private readonly List<IHost> _clientHosts = new();

    public WebSocketTestContext()
    {
        clientUri = new Uri($"http://localhost:{Port}/messages");
    }

    public async Task InitializeAsync()
    {
        var builder = WebApplication.CreateBuilder();

        builder.WebHost.ConfigureKestrel(opts =>
        {
            opts.ListenLocalhost(Port);
        });

snippet source | anchor

In the same test harness class, we bootstrap new IHost instances with the SignalR Client to mimic browser client communication like this:

cs
var host = await Host.CreateDefaultBuilder()
    .UseWolverine(opts =>
    {
        opts.ServiceName = serviceName;
        
        opts.UseClientToSignalR(Port);
        
        opts.PublishMessage<ToFirst>().ToSignalRWithClient(Port);
        
        opts.PublishMessage<RequiresResponse>().ToSignalRWithClient(Port);
        
        opts.Publish(x =>
        {
            x.MessagesImplementing<WebSocketMessage>();
            x.ToSignalRWithClient(Port);
        });
    }).StartAsync();

snippet source | anchor

The key point here is that we stood up the service using a port number for Kestrel, then stood up IHost instances for a Wolverine application using the SignalR Client using the same port number for easy connectivity.

And of course, after all of that we should probably talk about how to publish messages via the SignalR Client. Fortunately, there's really nothing to it. You merely need to invoke the normal IMessageBus.PublishAsync() APIs that you would use for any messaging. In the sample test below, we're utilizing the tracked session functionality as normal to send a message from the IHost hosting the SignalR Client transport and expect it to be successfully handled in the IHost for our actual SignalR server:

cs
[Fact]
public async Task receive_message_from_a_client()
{
    // This is an IHost that has the SignalR Client
    // transport configured to connect to a SignalR
    // server in the "theWebApp" IHost
    using var client = await StartClientHost();

    var tracked = await client
        .TrackActivity()
        .IncludeExternalTransports()
        .AlsoTrack(theWebApp)
        .Timeout(10.Seconds())
        .ExecuteAndWaitAsync(c => c.SendViaSignalRClient(clientUri, new ToSecond("Hollywood Brown")));

    var record = tracked.Received.SingleRecord<ToSecond>();
    record.ServiceName.ShouldBe("Server");
    record.Envelope.Destination.ShouldBe(new Uri("signalr://wolverine"));
    record.Message.ShouldBeOfType<ToSecond>()
        .Name.ShouldBe("Hollywood Brown");

}

snippet source | anchor

Conveniently enough as I write this documentation today using existing test code, Hollywood Brown had a huge game last night. Go Chiefs!

Web Socket "Sagas"

INFO

The functionality described in this section was specifically built for "CritterWatch" where a browser request kicks off a "scatter/gather" series of messages from CritterWatch to other Wolverine services and finally back to the originating browser client.

Let's say that you have a workflow in your system something like:

  1. The browser makes a web socket call to the server to request some information or take a long running action
  2. The server application needs to execute several messages or even call out to additional Wolverine services
  3. Once the server application has finally completed the work that the client requested, the server needs to send a message to the originating SignalR connection with the status of the long running activity or the data that the original client requested

The SignalR transport can leverage some of Wolverine's built in saga tracking to be able to route the eventual Web Socket response back to the originating caller even if the work required intermediate steps. The easiest way to enroll in this behavior today is the usage of the [EnlistInCurrentConnectionSaga] that should be on either

Released under the MIT License.