Index · How it works
1 min readRapid overview
How it works
SignalR is a library for ASP.NET that simplifies adding real-time web functionality. It provides an abstraction over WebSockets with automatic fallbacks and built-in features for connection management, broadcasting, and scaling.
Core SignalR Concepts
Hub - The Communication Endpoint
A Hub is a high-level pipeline that allows clients and servers to call methods on each other:
public class NotificationHub : Hub
{
public override async Task OnConnectedAsync()
{
var userId = Context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var tenantId = Context.User?.FindFirst("tenant_id")?.Value;
if (!string.IsNullOrEmpty(userId))
{
// Add user to their personal group
await Groups.AddToGroupAsync(Context.ConnectionId, $"user:{userId}");
}
if (!string.IsNullOrEmpty(tenantId))
{
// Add user to their tenant group for multi-tenant scenarios
await Groups.AddToGroupAsync(Context.ConnectionId, $"tenant:{tenantId}");
}
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
// Groups are automatically cleaned up on disconnect
await base.OnDisconnectedAsync(exception);
}
// Client can call this method
public async Task SendMessage(string message)
{
var userId = Context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
await Clients.Group($"user:{userId}").SendAsync("ReceiveMessage", message);
}
}
Groups - Targeted Broadcasting
Groups allow you to organize connections and send messages to specific subsets:
// Add connection to a group
await Groups.AddToGroupAsync(connectionId, "tenant:acme-corp");
// Send to all connections in a group
await Clients.Group("tenant:acme-corp").SendAsync("Notification", payload);
// Send to multiple groups
await Clients.Groups(new[] { "group1", "group2" }).SendAsync("Broadcast", data);
// Exclude specific connections
await Clients.GroupExcept("tenant:acme-corp", excludedConnectionIds)
.SendAsync("Notification", payload);
Connection Lifecycle
public class NotificationHub : Hub
{
private readonly IConnectionTracker _tracker;
public NotificationHub(IConnectionTracker tracker)
{
_tracker = tracker;
}
public override async Task OnConnectedAsync()
{
await _tracker.AddConnection(Context.ConnectionId, Context.User);
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
await _tracker.RemoveConnection(Context.ConnectionId);
if (exception != null)
{
// Log unexpected disconnection
// Consider sending notification about connection loss
}
await base.OnDisconnectedAsync(exception);
}
}
Client Implementation
JavaScript/TypeScript Client
import * as signalR from "@microsoft/signalr";
class NotificationService {
private connection: signalR.HubConnection;
private reconnectAttempts = 0;
private maxReconnectAttempts = 10;
constructor(private getAccessToken: () => Promise<string>) {
this.connection = new signalR.HubConnectionBuilder()
.withUrl("/hubs/notifications", {
accessTokenFactory: this.getAccessToken,
// Skip negotiation for WebSocket-only (faster connection)
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets
})
.withAutomaticReconnect({
nextRetryDelayInMilliseconds: (retryContext) => {
// Exponential backoff with jitter
if (retryContext.previousRetryCount >= this.maxReconnectAttempts) {
return null; // Stop reconnecting
}
const baseDelay = Math.min(1000 * Math.pow(2, retryContext.previousRetryCount), 30000);
const jitter = Math.random() * 1000;
return baseDelay + jitter;
}
})
.configureLogging(signalR.LogLevel.Information)
.build();
this.setupEventHandlers();
}
private setupEventHandlers(): void {
this.connection.on("ReceiveNotification", (notification: Notification) => {
this.handleNotification(notification);
});
this.connection.onreconnecting((error) => {
console.log("Reconnecting...", error);
this.updateConnectionStatus("reconnecting");
});
this.connection.onreconnected((connectionId) => {
console.log("Reconnected with ID:", connectionId);
this.updateConnectionStatus("connected");
this.reconnectAttempts = 0;
});
this.connection.onclose((error) => {
console.log("Connection closed", error);
this.updateConnectionStatus("disconnected");
});
}
async start(): Promise<void> {
try {
await this.connection.start();
console.log("SignalR Connected");
this.updateConnectionStatus("connected");
} catch (err) {
console.error("SignalR Connection Error:", err);
// Retry after delay
setTimeout(() => this.start(), 5000);
}
}
async stop(): Promise<void> {
await this.connection.stop();
}
private handleNotification(notification: Notification): void {
// Dispatch to notification system
// Show toast, update UI, etc.
}
private updateConnectionStatus(status: string): void {
// Update UI to show connection status
}
}
.NET Client
public class SignalRNotificationClient : IAsyncDisposable
{
private readonly HubConnection _connection;
private readonly ILogger<SignalRNotificationClient> _logger;
public event Action<Notification>? OnNotificationReceived;
public SignalRNotificationClient(
string hubUrl,
Func<Task<string>> accessTokenProvider,
ILogger<SignalRNotificationClient> logger)
{
_logger = logger;
_connection = new HubConnectionBuilder()
.WithUrl(hubUrl, options =>
{
options.AccessTokenProvider = accessTokenProvider;
options.Transports = HttpTransportType.WebSockets;
options.SkipNegotiation = true;
})
.WithAutomaticReconnect(new ExponentialBackoffRetryPolicy())
.Build();
_connection.On<Notification>("ReceiveNotification", notification =>
{
OnNotificationReceived?.Invoke(notification);
});
_connection.Reconnecting += error =>
{
_logger.LogWarning(error, "Connection lost, reconnecting...");
return Task.CompletedTask;
};
_connection.Reconnected += connectionId =>
{
_logger.LogInformation("Reconnected with ID: {ConnectionId}", connectionId);
return Task.CompletedTask;
};
}
public async Task StartAsync(CancellationToken cancellationToken = default)
{
await _connection.StartAsync(cancellationToken);
}
public async ValueTask DisposeAsync()
{
await _connection.DisposeAsync();
}
}
public class ExponentialBackoffRetryPolicy : IRetryPolicy
{
private readonly int _maxRetries = 10;
public TimeSpan? NextRetryDelay(RetryContext retryContext)
{
if (retryContext.PreviousRetryCount >= _maxRetries)
return null;
var delay = TimeSpan.FromSeconds(Math.Pow(2, retryContext.PreviousRetryCount));
var jitter = TimeSpan.FromMilliseconds(Random.Shared.Next(0, 1000));
return delay + jitter;
}
}