Skip to main content

SignalR Hub Management

Centralized hub route registration and URL generation with support for multi-tenant path prefixes.

Quick Start

builder.Services.AddCoreSignalR();

Why This Abstraction?

In a standard ASP.NET Core application, SignalR hub paths are hardcoded at startup:

app.MapHub<ChatHub>("/chatHub");
  1. Discover the current tenant's URL prefix
  2. Build the correct hub path with that prefix
  3. Expose the full URL to client-side JavaScript for connection

The HubRouteManager solves all three problems by providing a single service that:

  • Centralizes path construction — one place handles the prefix logic
  • Generates correct URLs — both relative paths (for MapHub) and absolute URIs (for JavaScript clients)
  • Prevents path conflicts — all hubs follow the same /Communication/Hub/{HubName} pattern

Without this, you would see bugs like tenant A's JavaScript connecting to tenant B's hub, or paths breaking after deployment behind a reverse proxy.

Problem & Solution

SignalR hubs need consistent route registration and URL generation across features. In multi-tenant environments, hub paths must include a tenant prefix. The HubRouteManager centralizes this so individual features don't manage paths independently.

Real-time Chat Example

The primary consumer of HubRouteManager is the AI Chat system. Here is how the pieces fit together:

Server-side: Hub Registration

// In the Chat module's Startup.Configure():
public override void Configure(
IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
{
// Maps the AIChatHub at /Communication/Hub/AIChatHub (with tenant prefix)
HubRouteManager.MapHub<AIChatHub>(routes);
}

Server-side: URL Generation for Client

// In a Razor view or controller, generate the hub URL for the client:
public class ChatController(HubRouteManager hubRouteManager)
{
public IActionResult Index()
{
var hubUrl = hubRouteManager.GetUriByHub<AIChatHub>(HttpContext);
// Returns: "https://example.com/tenant-a/Communication/Hub/AIChatHub"

ViewBag.ChatHubUrl = hubUrl;
return View();
}
}

Client-side: JavaScript Connection

// Connect to the hub using the URL generated server-side
const connection = new signalR.HubConnectionBuilder()
.withUrl(chatHubUrl) // URL from the server
.withAutomaticReconnect()
.build();

// Listen for streamed AI responses
connection.on("ReceiveMessage", (update) => {
appendMessage(update.text);
});

// Send a message
await connection.invoke("SendMessage", {
profileId: "my-profile",
sessionId: sessionId,
message: userInput,
});

await connection.start();

How Streaming Works

When a user sends a message, the AIChatHub processes it through the response handler pipeline. For the default AI handler, the response is streamed token-by-token:

Client                          AIChatHub                       Orchestrator
│ │ │
│── SendMessage(msg) ──────────▶│ │
│ │── ExecuteStreamingAsync() ───▶│
│ │ │
│ │◀── ChatResponseUpdate ────────│
│◀── ReceiveMessage(chunk1) ────│ │
│ │◀── ChatResponseUpdate ────────│
│◀── ReceiveMessage(chunk2) ────│ │
│ │◀── (stream complete) ─────────│
│◀── MessageCompleted ──────────│ │

Hub Registration

Registering a Custom Hub

To register your own SignalR hub that works correctly in multi-tenant environments:

1. Define your hub:

public sealed class NotificationHub : Hub
{
public async Task Subscribe(string topic)
{
await Groups.AddToGroupAsync(Context.ConnectionId, topic);
}

public async Task Broadcast(string topic, string message)
{
await Clients.Group(topic).SendAsync("Notify", message);
}
}

2. Map it using HubRouteManager:

public sealed class Startup : StartupBase
{
public override void Configure(
IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
{
// The static MapHub<T> method uses the default path pattern
HubRouteManager.MapHub<NotificationHub>(routes);
// Maps to: /{tenant-prefix}/Communication/Hub/NotificationHub
}
}

3. Generate the URL for clients:

public class MyService(HubRouteManager hubRouteManager)
{
public string GetNotificationHubUrl(HttpContext httpContext)
{
return hubRouteManager.GetUriByHub<NotificationHub>(httpContext);
}
}

Scale-out with Redis Backplane

By default, SignalR keeps all connection state in-memory on a single server. In a multi-server deployment, messages sent on one server won't reach clients connected to another server.

The solution is a Redis backplane, which broadcasts SignalR messages across all servers:

Server 1 ──┐                ┌── Server 2
│ │
▼ ▼
┌─────────────────────┐
│ Redis Backplane │
└─────────────────────┘

Configure the Redis connection in your environment:

appsettings.json
{
"Configuration": "localhost:6379,allowAdmin=true"
}
}
}

Or via environment variables:

info

When using the Aspire AppHost for local development, Redis is configured automatically as part of the orchestration. See the Aspire project at src/Startup/CrestApps.Core.Aspire.AppHost/.

When You Need Scale-out

  • Single server: No backplane needed. SignalR works out of the box.
  • Multiple servers behind a load balancer: Redis backplane required for message delivery across servers.
  • Azure App Service with multiple instances: Enable Redis or use Azure SignalR Service.

Services Registered by AddCoreSignalR()

ServiceImplementationLifetimePurpose
HubRouteManagerSingletonHub route registration and URL generation

Configuration

Path Prefix (Multi-Tenant)

builder.Services.AddCoreSignalR(pathPrefix: "/tenant-a");

All hub routes will be prefixed with the tenant path.

Using the Hub Route Manager

Mapping Hubs

var app = builder.Build();

// Map hubs during endpoint routing
app.MapHub<AIChatHub>(
app.Services.GetRequiredService<HubRouteManager>().GetPathByHub<AIChatHub>());

Generating URLs

public class MyService(HubRouteManager hubRouteManager)
{
public string GetChatHubUrl(HttpContext httpContext)
{
return hubRouteManager.GetUriByHub<AIChatHub>(httpContext);
// Returns: "https://example.com/tenant-a/Communication/Hub/AIChatHub"
}
}

Default Hub Path Pattern

/Communication/Hub/{HubName}

With a prefix of /tenant-a:

/tenant-a/Communication/Hub/{HubName}

Key Methods

MethodDescription
GetPathByHub<T>()Get the route path for a hub type
GetPathByRoute(pattern)Get the route path with prefix applied
GetUriByHub<T>(httpContext)Full URI including scheme and host
GetUriByRoute(httpContext, pattern)Full URI for a custom route pattern