Real-Time or Real Pain? Wrangling SignalR Without Falling to the Dark Side
2025-04-28
#signalr#aspnet-core#realtime#tutorial
TL;DR 👉 “SignalR is like the Millennium Falcon: it makes the Kessel Run in under twelve parsecs, but you’ll be under the console with a hydrospanner every other jump.”
1. Why SignalR? …and why it sometimes bites like a Nexu
- Real-time over WebSockets out-of-the-box.
- Automatic transport fallback.
- Strongly-typed hubs with
async/await
. - Built-in scale-out via Redis or Azure SignalR Service.
2. Prerequisites Imperial checklist edition
Tool / Lib | Version (min) | Why you need it | Install |
---|---|---|---|
.NET 8 SDK | 8.0.x |
LTS → less Holocrons to consult | winget install Microsoft.DotNet.SDK.8 |
Node.js | 20.x |
For the TypeScript/JS client | nvm install 20 && nvm use 20 |
Visual Studio 2022 or VS Code | latest | Because Notepad.exe can’t debug a hub | ¯\(ツ)/¯ |
Redis (optional) | 7.x |
Horizontal scaling | docker run -p 6379:6379 redis:7-alpine |
3. Spinning Up the Server “Punch it, Chewie!”
dotnet new web -n DeathStar.Comms.Center
cd DeathStar.Comms.Center
dotnet add package Microsoft.AspNetCore.SignalR
// DeathStar.Comms.Center/DeathStarHub.cs
using Microsoft.AspNetCore.SignalR;
public class DeathStarHub : Hub
{
public async Task Fire(string targetSystem, int powerPercent)
{
await Clients.All.SendAsync("OnSuperlaserFired", targetSystem, powerPercent);
}
public override Task OnConnectedAsync()
{
Console.WriteLine($"🎧 {Context.ConnectionId} online");
return base.OnConnectedAsync();
}
}
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
var app = builder.Build();
app.MapHub<DeathStarHub>("/deathstarHub");
app.Run();
Heads-up: If you see
HTTP 404
on/deathstarHub/negotiate
, your middleware order is wrong.
4. Talking to Droids (JS / TS Client)
pnpm add @microsoft/signalr
// /public/superlaser.js
import { HubConnectionBuilder, LogLevel } from "@microsoft/signalr";
const hub = new HubConnectionBuilder()
.withUrl("/deathstarHub")
.configureLogging(LogLevel.Information)
.withAutomaticReconnect([0, 2000, 5000])
.build();
hub.on("OnSuperlaserFired", (system, power) => {
console.log(`💥 Superlaser aimed at ${system} @ ${power}%`);
});
await hub
.start()
.then(() => hub.invoke("Fire", "Alderaan", 100))
.catch((err) => console.error("Lost in hyperspace:", err));
5. Dealing with Disconnects The Sarlacc Pit of Real-Time Apps
builder.Services.AddSignalR(options =>
{
options.KeepAliveInterval = TimeSpan.FromSeconds(10);
options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
});
6. Authentication & Authorization “These aren’t the access tokens you’re looking for.”
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = …;
options.Events = new JwtBearerEvents
{
OnMessageReceived = ctx =>
{
var accessToken = ctx.Request.Query["access_token"];
var path = ctx.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/deathstarHub"))
ctx.Token = accessToken;
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
7. Scaling Out to an Entire Imperial Fleet
Redis Backplane (self-hosted)
builder.Services.AddSignalR()
.AddStackExchangeRedis("redis:6379", options =>
{
options.Configuration.ChannelPrefix = "deathstar";
});
Azure SignalR Service
builder.Services.AddSignalR()
.AddAzureSignalR();
8. Testing & Debugging Without Blowing Up a Planet
- Browser dev tools → Check if WebSocket opens (
101 Switching Protocols
). HubConnection
logging withTrace
level.dotnet trace
and EventPipe for deep backend debugging.- Unit test hubs using in-memory servers.
9. Common Gotchas
Mistake | Symptom | Fix |
---|---|---|
Invoking hub methods from Dispose |
Swallowed exceptions, ghost connections | Keep hub calls off finalizers. |
Large payloads or binary blobs | Messages truncated by proxies | Use external storage, send URLs. |
Multiple HubConnection per tab |
RAM consumption skyrockets | Use a singleton pattern. |
Ignoring backpressure | Redis CPU spikes, broadcast lag | Use groups and throttle messages. |
10. Next Steps
- Switch to MessagePack.
- Add Presence detection.
- Integrate OpenTelemetry.
Final Thought
“The ability to send a million WebSocket frames is insignificant next to the power of readable logs and idempotent server code.”
— Probably Darth Vader, after debugging someone else’s SignalR hub.
May your connections stay open and your logs stay clean!
Last updated: 2025-04-28