redb.Route.SignalR 4.1.0

Prefix Reserved
dotnet add package redb.Route.SignalR --version 4.1.0
                    
NuGet\Install-Package redb.Route.SignalR -Version 4.1.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="redb.Route.SignalR" Version="4.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="redb.Route.SignalR" Version="4.1.0" />
                    
Directory.Packages.props
<PackageReference Include="redb.Route.SignalR" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add redb.Route.SignalR --version 4.1.0
                    
#r "nuget: redb.Route.SignalR, 4.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package redb.Route.SignalR@4.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=redb.Route.SignalR&version=4.1.0
                    
Install as a Cake Addin
#tool nuget:?package=redb.Route.SignalR&version=4.1.0
                    
Install as a Cake Tool

redb.Route.SignalR

SignalR transport for the redb.Route ESB framework.

Provides an embedded Kestrel-based SignalR Hub consumer (server) and two producer modes: client (HubConnection to remote hub) and server (IHubContext broadcast to connected clients).

Supports JSON and MessagePack protocols, WebSocket / SSE / LongPolling transports, group management, InOut exchange pattern, lifecycle events, auto-reconnect, and TLS.

Known issue: .NET 9. In redb.Route 4.0.0 and 4.0.1 the hub the consumer hosts closes client connections with "Connection closed with an error" when the application runs on net9.0, so the transport does not work there. net8.0 and net10.0 are not affected. Until a release with the fix, run SignalR routes on .NET 8 or .NET 10.

NuGet License: Apache 2.0

Scheme signalr
NuGet redb.Route.SignalR
Dependencies Microsoft.AspNetCore.App, Microsoft.AspNetCore.SignalR.Client 9.0.3
Namespace redb.Route.SignalR

Quick Start

services.AddRedbRoute(route =>
{
    route.Services.AddRedbRouteSignalR();

    // Hub server — accept connections, process method invocations
    route.AddRoute("signalr-server", r => r
        .From(SignalR.Hub("0.0.0.0:5000/chatHub").InOut())
        .Process(e =>
        {
            var method = e.In.Headers["redbSignalR.Method"];
            e.Out = new Message($"Echo: {e.In.Body}");
        }));

    // Client — connect to a remote hub and send messages
    route.AddRoute("signalr-client", r => r
        .From("direct:send")
        .To(SignalR.Connect("api.example.com:5000/chatHub")
            .Method("Send").Reconnect()));

    // Server broadcast — push to all connected clients of the local hub
    route.AddRoute("signalr-broadcast", r => r
        .From("direct:notify")
        .To(SignalR.Broadcast("0.0.0.0:5000/chatHub")
            .Method("Notify")));
});

Architecture

Consumer (Hub Server)

The consumer serves a RedbBridgeHub — an internal bridge hub that converts all SignalR invocations into redb.Route exchanges — on the shared Kestrel host (redb.Route.Http.Hosting), the same listener HTTP, gRPC, SOAP and AS2 routes use. So the hub and the REST API it pushes for sit on one port, behind one proxy and one TLS termination:

From("http://0.0.0.0:8080/api/orders")   // REST
From("signalr://0.0.0.0:8080/hub")       // push, same port
From("signalr://0.0.0.0:8080/admin")     // another hub, same port again

Several hubs on one listener are routed to their own consumer by request path.

Clients call Invoke("MethodName", arg1, arg2, ...) on the hub. The hub creates an exchange with the method name and arguments, processes it through the route pipeline, and optionally returns the Out body as the method result (when InOut is enabled).

Lifecycle events (OnConnectedAsync, OnDisconnectedAsync) are also dispatched as exchanges with the redbSignalR.Event header set to "Connected" or "Disconnected".

Producer — Client Mode (default)

Uses HubConnection to connect to a remote SignalR hub and invoke methods.

Two sub-modes:

  • Bridge (default, bridge=true) — calls Invoke(method, args) on the bridge hub entry point. Used when connecting to another redb.Route hub.
  • Direct (bridge=false) — calls the hub method directly by name. Used when connecting to external (third-party) SignalR hubs with real named methods.

Producer — Server Mode

Uses IHubContext<RedbBridgeHub> to broadcast messages to clients connected to the local hub. Requires a consumer running on the same endpoint. Target audience configurable: All, Group, User, Connection.


URI Format

signalr:host:port/hubPath?option=value&...

Consumer (hub server):

signalr:0.0.0.0:5000/chatHub?inOut=true&messagePack=true&defaultGroup=lobby

Producer — Client (connect to remote):

signalr:api.example.com:5000/chatHub?mode=client&method=Send&reconnect=true

Producer — Server (broadcast to local clients):

signalr:0.0.0.0:5000/chatHub?mode=server&method=Notify&targetType=group&targetGroup=room1

DSL

using redb.Route.SignalR;

// Consumer — start hub server
SignalR.Hub("0.0.0.0:5000/chatHub")
    .InOut()
    .MessagePack()
    .DefaultGroup("lobby")

// Producer — connect to remote hub (client mode)
SignalR.Connect("api.example.com:5000/chatHub")
    .Method("Send")
    .Reconnect(intervalMs: 3000, maxAttempts: 10)
    .AccessToken("jwt-token")
    .Ssl()

// Producer — connect to external (non-redb) hub
SignalR.Connect("external.host:5000/hub")
    .Method("SendMessage")
    .Direct()

// Producer — broadcast to local hub clients (server mode)
SignalR.Broadcast("0.0.0.0:5000/chatHub")
    .Method("Notify")
    .Group("room1")

The builder implements implicit operator string — pass directly to .From() / .To().


Exchange Pattern

InOnly (default)

Client invocations are fire-and-forget. The hub returns null.

InOut

When inOut=true, the consumer returns the exchange.Out.Body as the hub method result:

route.AddRoute("rpc", r => r
    .From(SignalR.Hub("0.0.0.0:5000/rpcHub").InOut())
    .Process(e =>
    {
        var input = (long)e.In.Body!;
        e.Out = new Message(input * 2);
    }));

Client calls Invoke("Multiply", 21) → receives 42.


Group Management

Default Group

Auto-join all connections to a group on connect:

SignalR.Hub("0.0.0.0:5000/chatHub").DefaultGroup("lobby")

Dynamic Group Management

Set headers on the exchange Out (or In) to add/remove the current connection from groups:

Header Action
redbSignalR.AddToGroup Add connection to the named group
redbSignalR.RemoveFromGroup Remove connection from the named group
.Process(e =>
{
    e.Out = new Message("Joined room");
    e.Out.Headers["redbSignalR.AddToGroup"] = "premium-users";
})

Groups do not survive a reconnect

A group membership belongs to a connection, and a reconnect is a new connection with a new id, so whatever the client joined is gone. SignalR does not restore it and neither can a backplane — nobody remembers who was in what. Two ways to put it back, both from the connector:

// Everyone lands in the same group: declare it, and every connection joins on connect.
SignalR.Hub("0.0.0.0:5000/chatHub").DefaultGroup("lobby")

// Membership depends on who is connecting: rejoin on the Connected event, which the hub
// dispatches as an exchange like any other.
route.AddRoute("rejoin", r => r
    .From(SignalR.Hub("0.0.0.0:5000/chatHub"))
    .Filter(Header(SignalRHeaders.Event).isEqualTo("Connected"))
    .Process(async (e, ct) =>
    {
        var user = e.In.GetHeader<string>(SignalRHeaders.UserId);
        e.Out = new Message(null);
        e.Out.Headers["redbSignalR.AddToGroup"] = await groups.ResolveFor(user);
    }));

Server-Mode Broadcasting

Target audiences for server-mode producer:

TargetType Header Override Description
All (default) redbSignalR.Target = "All" Send to all connected clients
Group redbSignalR.Group = "name" Send to a specific group
User redbSignalR.TargetUser = "userId" Send to a specific user
Connection redbSignalR.TargetConnection = "connId" Send to a specific connection
// Broadcast to a specific group
route.AddRoute("group-notify", r => r
    .From("direct:alert")
    .SetHeader("redbSignalR.Group", "admins")
    .To(SignalR.Broadcast("0.0.0.0:5000/chatHub")
        .Method("Alert")));

Server mode pushes through the hub of the consumer serving the same address, so the consumer has to be running: a producer that finds none refuses to start rather than pushing into nothing. In a route context that happens by itself — consumers start with the context, producers resolve lazily on the first message.


Reconnect (Client Mode)

SignalR.Connect("host:5000/hub")
    .Method("Send")
    .Reconnect(intervalMs: 5000, maxAttempts: 0)  // 0 = unlimited

Uses a fixed-interval retry policy. When connection drops and Reconnect is enabled, the producer automatically re-establishes the connection before sending.


TLS

Consumer (Server)

SignalR.Hub("0.0.0.0:5443/hub")
    .Ssl()
    .SslCertPath("/certs/server.pfx")
    .SslCertPassword("password")

Configures Kestrel to listen with HTTPS using the PFX certificate.

Producer (Client)

SignalR.Connect("host:5443/hub")
    .Ssl()

Switches the connection URL scheme to https://.

ssl=true on a consumer requires sslCertPath: without it the listener would be plain HTTP while the log said https://, so the consumer refuses to start instead.

To reach a hub with a self-signed certificate (staging), say so out loud:

SignalR.Connect("staging:5443/hub").Ssl().TrustAllCertificates()

It is never implied by anything else, and it disables certificate validation for the whole transport — negotiate, redirects and the WebSocket upgrade alike.


Authentication

Producer (client) — sending a token

SignalR.Connect("host:5000/hub")
    .Method("Send")
    .AccessToken("eyJhbGciOiJIUzI1...")

Passed via AccessTokenProvider on the HubConnection.

Consumer (hub) — validating one

The process hosting a route context is a generic host, not an ASP.NET application, so there is no AddAuthentication/AddJwtBearer to hang the hub off. The host supplies a delegate instead:

services.AddRedbRouteSignalR(o =>
{
    o.Authenticate = async ctx =>
    {
        // A browser cannot set headers on a WebSocket handshake, so the token normally arrives
        // in the query string; a service client will use the header. Read both.
        var header = ctx.Request.Headers.Authorization.ToString();
        var token = header.StartsWith("Bearer ")
            ? header["Bearer ".Length..]
            : ctx.Request.Query["access_token"].ToString();

        return await myJwtValidator.ValidateAsync(token);   // null → 401
    };
});

Returning null rejects the handshake with 401 before the connection is upgraded. The principal's NameIdentifier claim becomes SignalR's UserIdentifier, which is what makes Clients.User(...) (targetType=User) and the redbSignalR.UserId header work. The principal itself is on every exchange the hub produces (ExchangePrincipal.Get(exchange)); an anonymous connection carries none. Build the identity with an authentication type (new ClaimsIdentity(claims, "Bearer")): code that reads the principal treats an identity that is not authenticated as anonymous.

Without this delegate, a caller identified by the shared host (AddRedbRouteHttpHosting(o => o.ResolvePrincipal = ...)) is handed to SignalR the same way, but an anonymous handshake is not rejected. When the delegate is set, it takes precedence.


Scale-out: a backplane is mandatory with more than one replica

A hub keeps its connections and groups in the memory of one process. With two replicas behind a load balancer, a broadcast from one reaches only the clients attached to that one — the other replica's clients hear nothing.

The connector does not depend on Redis or Azure SignalR; it publishes the seam and the host fills it in:

services.AddRedbRouteSignalR(o =>
    o.ConfigureHubServices(s => s.AddSignalR().AddStackExchangeRedis("redis:6379")));

The container this configures belongs to the shared listener and does not see the services of the application hosting the route context, so anything the backplane needs must be registered here.


Configuration Reference

Parameter Type Default Description
host string "0.0.0.0" Bind/connect host (parsed from URI path)
port int 5000 Bind/connect port (parsed from URI path)
mode enum Client Producer mode: Client or Server
method string null Hub method to invoke/filter
inOut bool false Request-response exchange pattern
bridge bool true Route through bridge hub (client mode). false = direct method calls
transport enum WebSockets WebSockets, ServerSentEvents, LongPolling
messagePack bool false Register the MessagePack protocol beside JSON (JSON stays the base, so existing clients keep working)
defaultGroup string null Auto-join group on connect (consumer)
targetType string "All" Broadcast target: All, Group, User, Connection (server mode)
targetGroup string null Default target group (server mode)
ssl bool false Enable TLS. On a consumer, requires sslCertPath
sslCertPath string null PFX certificate path (consumer)
sslCertPassword string null PFX certificate password
trustAllCertificates bool false Producer: accept any server certificate (self-signed staging). Explicit, never implied
reconnect bool false Auto-reconnect on disconnect (client mode)
reconnectInterval int 5000 Reconnect interval in ms
maxReconnectAttempts int 0 Max reconnect attempts (0 = unlimited: WithAutomaticReconnect keeps retrying in the background, which is its normal mode)
accessToken string null JWT token for client auth

Headers Reference

Common (set by consumer)

Header Description
redbSignalR.Method Hub method name
redbSignalR.ConnectionId SignalR connection ID
redbSignalR.UserId Authenticated user ID
redbSignalR.Event Lifecycle event: "Connected" / "Disconnected"
redbSignalR.HubPath Hub path (e.g. /chatHub)
redbSignalR.Protocol "json" or "messagepack"
redbSignalR.Ssl "True" / "False"
redbSignalR.DisconnectError Error message from disconnection

Producer Targeting

Header Description
redbSignalR.Target Broadcast audience: "All", "Group", "User", "Connection"
redbSignalR.Group Target group name
redbSignalR.TargetConnection Target connection ID
redbSignalR.TargetUser Target user ID

Group Management (post-processing commands)

Header Description
redbSignalR.AddToGroup Add current connection to specified group
redbSignalR.RemoveFromGroup Remove current connection from specified group

DI Registration

services.AddRedbRoute(route =>
{
    route.Services.AddRedbRouteSignalR();
    // ...
});

Registers the SignalRComponent (scheme signalr).

Named connection factory

Keep credentials out of the route URI: register a factory in the context registry and reference it by name. A set-but-unknown name fails loud at startup — a typo can never silently fall back to inline URI parameters.

context.AddToRegistry("prod", new SignalRConnectionFactory
{
    AccessToken = secrets.HubToken,
});
// signalr://https://hub.internal/notify?connectionFactory=prod

Concurrency limits

SignalR concurrency is connections × invocations, so the levers differ from plain HTTP:

Parameter Default Description
maxConnections 0 (unlimited) Max hub connections; an over-limit connection is aborted at OnConnected — before the Connected lifecycle event reaches the pipeline — and counted in the endpoint's Rejected
maxParallelInvocationsPerClient 0 (SignalR default = 1) SignalR's own per-client invocation parallelism

With the defaults one client's invocations are serial (SignalR's own default) and the number of clients is unbounded; maxConnections is the coarse lever, the per-client setting the fine one.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 is compatible.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.1.0 0 9/21/2026
4.0.1 46 9/18/2026
4.0.0 95 9/11/2026
3.7.2 111 8/26/2026
3.7.1 105 8/26/2026
3.6.0 99 8/13/2026
3.5.1 104 8/9/2026
3.5.0 104 8/6/2026
3.4.0 113 7/27/2026
3.3.3 112 7/16/2026
3.3.1 121 7/10/2026
3.3.0 115 7/8/2026
3.2.0 136 6/29/2026
3.1.0 129 6/6/2026
3.0.1 113 6/3/2026
3.0.0 120 5/29/2026
2.0.2 115 5/16/2026
2.0.0 78 5/6/2026