Loading awesome stuff...

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 with Trace 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