SignalR Fundamentals — Hubs, Connections and Transport Fallback

SignalR is ASP.NET Core’s library for adding real-time bidirectional communication to web applications. It abstracts over transport protocols — WebSockets (preferred), Server-Sent Events, and Long Polling — and automatically falls back based on browser and network support. A Hub is the server-side class that coordinates communication: clients invoke Hub methods (client → server), and the Hub invokes methods on clients (server → client). For the Angular full-stack application, SignalR enables live notifications when posts are published, real-time comment updates, and presence features without polling.

SignalR Setup

// dotnet add package Microsoft.AspNetCore.SignalR

// ── Register SignalR ───────────────────────────────────────────────────────
builder.Services.AddSignalR(opts =>
{
    opts.EnableDetailedErrors    = app.Environment.IsDevelopment();
    opts.KeepAliveInterval       = TimeSpan.FromSeconds(15);
    opts.ClientTimeoutInterval   = TimeSpan.FromSeconds(30);
    opts.MaximumReceiveMessageSize = 32 * 1024;   // 32KB max message size
})
.AddJsonProtocol(opts =>
{
    // Consistent camelCase naming with the REST API JSON settings
    opts.PayloadSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
// Optional: .AddMessagePackProtocol() for binary (smaller + faster)

// ── CORS — must explicitly allow SignalR origin and credentials ────────────
builder.Services.AddCors(opts =>
    opts.AddPolicy("AllowAngular", policy =>
        policy.WithOrigins("http://localhost:4200")
              .AllowAnyMethod()
              .AllowAnyHeader()
              .AllowCredentials()));   // required for WebSocket upgrade

// ── Map the Hub endpoint ──────────────────────────────────────────────────
app.UseCors("AllowAngular");   // must be before MapHub
app.UseAuthentication();
app.UseAuthorization();
app.MapHub<NotificationHub>("/hubs/notifications");
app.MapHub<PostHub>("/hubs/posts");

// ── Hub — the real-time communication endpoint ────────────────────────────
public class NotificationHub : Hub
{
    private readonly ILogger<NotificationHub> _logger;
    public NotificationHub(ILogger<NotificationHub> logger) => _logger = logger;

    // Called automatically when a client connects
    public override async Task OnConnectedAsync()
    {
        _logger.LogInformation("Client connected: {ConnectionId}", Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    // Called automatically when a client disconnects
    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        _logger.LogInformation("Client disconnected: {ConnectionId} ({Reason})",
            Context.ConnectionId, exception?.Message ?? "clean");
        await base.OnDisconnectedAsync(exception);
    }

    // Hub method — Angular client invokes this
    public async Task SendMessage(string message)
    {
        // Broadcast to ALL connected clients
        await Clients.All.SendAsync("ReceiveMessage", message, Context.ConnectionId);
    }
}
Note: SignalR’s transport fallback order is: WebSockets → Server-Sent Events → Long Polling. WebSockets provide true bidirectional communication and are supported by all modern browsers. Server-Sent Events allow server-to-client push only (one-directional, but simpler). Long Polling simulates push by having the client repeatedly poll the server. In practice, WebSockets work in all modern environments — the fallback is mainly for corporate firewalls that block WebSocket upgrades. Configure opts.Transports = HttpTransportType.WebSockets to WebSocket-only if you know your environment supports it.
Tip: Use EnableDetailedErrors = app.Environment.IsDevelopment() to get detailed SignalR error messages in development (hub method names, exception messages) while keeping them suppressed in production. Without this, SignalR surfaces generic errors on the Angular side that make debugging difficult. The detailed errors are very helpful during development but should never reach production as they may expose internal implementation details.
Warning: AllowCredentials() is required in the CORS policy for SignalR WebSocket connections. Without it, the WebSocket upgrade request from Angular is blocked by the browser’s CORS policy — SignalR silently falls back to Long Polling, which is significantly less efficient. If you see SignalR connections falling back to polling in development, check the browser’s Network tab for failed pre-flight requests and verify AllowCredentials() is in the CORS policy.

Transport Types and Selection

Transport Direction Support Efficiency
WebSockets Bidirectional All modern browsers ✅ Best
Server-Sent Events Server → Client only All except IE Good
Long Polling Bidirectional (simulated) All browsers Worst (high overhead)

Common Mistakes

Mistake 1 — Missing AllowCredentials() in CORS (WebSockets fail, falls back to polling)

❌ Wrong — CORS policy without AllowCredentials(); WebSocket upgrade fails; Angular uses polling.

✅ Correct — always include .AllowCredentials() when using SignalR with CORS.

Mistake 2 — Injecting Scoped services into Hub constructor (Hub scope issues)

❌ Wrong — Hub is created per invocation; Scoped services may behave unexpectedly.

✅ Correct — Hubs support constructor injection; services resolve from the current scope per connection.

🧠 Test Yourself

An Angular client connects to a SignalR Hub. The browser console shows the transport type is “LongPolling” instead of “WebSockets”. What is the most likely cause?