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
<PackageReference Include="redb.Route.SignalR" Version="4.1.0" />
<PackageVersion Include="redb.Route.SignalR" Version="4.1.0" />
<PackageReference Include="redb.Route.SignalR" />
paket add redb.Route.SignalR --version 4.1.0
#r "nuget: redb.Route.SignalR, 4.1.0"
#:package redb.Route.SignalR@4.1.0
#addin nuget:?package=redb.Route.SignalR&version=4.1.0
#tool nuget:?package=redb.Route.SignalR&version=4.1.0
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.0andnet10.0are not affected. Until a release with the fix, run SignalR routes on .NET 8 or .NET 10.
| 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) — callsInvoke(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 | Versions 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. |
-
net10.0
- MessagePack (>= 3.1.8)
- Microsoft.AspNetCore.SignalR.Client (>= 9.0.3)
- Microsoft.AspNetCore.SignalR.Protocols.MessagePack (>= 10.0.11)
- redb.Route (>= 4.1.0)
- redb.Route.Http.Hosting (>= 4.1.0)
-
net8.0
- MessagePack (>= 3.1.8)
- Microsoft.AspNetCore.SignalR.Client (>= 9.0.3)
- Microsoft.AspNetCore.SignalR.Protocols.MessagePack (>= 10.0.11)
- redb.Route (>= 4.1.0)
- redb.Route.Http.Hosting (>= 4.1.0)
-
net9.0
- MessagePack (>= 3.1.8)
- Microsoft.AspNetCore.SignalR.Client (>= 9.0.3)
- Microsoft.AspNetCore.SignalR.Protocols.MessagePack (>= 10.0.11)
- redb.Route (>= 4.1.0)
- redb.Route.Http.Hosting (>= 4.1.0)
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 |