dotnet-signalr ยท diff
v1.0.0 to v1.0.0
305 added, 1 removed. Audit A to A.
---
name: dotnet-signalr
version: "1.0.0"
- category: "Web and Cloud"
+ category: "Web"
description: "Implement or review SignalR hubs, streaming, reconnection, transport, and real-time delivery patterns in ASP.NET Core applications."
compatibility: "Requires ASP.NET Core SignalR server or client code."
---
# SignalR
## Trigger On
- building chat, notification, collaboration, or live-update features
- debugging hub lifetime, connection state, or transport issues
- deciding whether SignalR or another transport better fits the scenario
+ - implementing real-time broadcasting to groups of connected clients
+ - scaling SignalR across multiple servers
+ ## Documentation
+
+ - [ASP.NET Core SignalR Overview](https://learn.microsoft.com/en-us/aspnet/core/signalr/introduction?view=aspnetcore-10.0)
+ - [SignalR Hubs](https://learn.microsoft.com/en-us/aspnet/core/signalr/hubs?view=aspnetcore-10.0)
+ - [SignalR API Design Considerations](https://learn.microsoft.com/en-us/aspnet/core/signalr/api-design?view=aspnetcore-10.0)
+ - [SignalR Production Hosting and Scaling](https://learn.microsoft.com/en-us/aspnet/core/signalr/scale?view=aspnetcore-10.0)
+ - [SignalR Configuration](https://learn.microsoft.com/en-us/aspnet/core/signalr/configuration?view=aspnetcore-10.0)
+
+ ### References
+
+ - [patterns.md](references/patterns.md) - Detailed hub patterns, streaming, groups, presence, and advanced messaging techniques
+ - [anti-patterns.md](references/anti-patterns.md) - Common SignalR mistakes and how to avoid them
+
## Workflow
1. Use SignalR for broadcast-style or connection-oriented real-time features; do not force gRPC into hub-style fan-out scenarios.
2. Model hub contracts intentionally and keep hub methods thin, delegating durable work elsewhere.
3. Plan for reconnection, backpressure, auth, and fan-out costs instead of treating real-time messaging as stateless request/response.
4. Use groups, presence, and connection metadata deliberately so scale-out behavior is understandable.
5. If Native AOT or trimming is in play, validate supported protocols and serialization choices explicitly.
6. Test connection behavior and failure modes, not just happy-path message delivery.
+ ## Hub Patterns
+
+ ### Strongly-Typed Hub (Recommended)
+ ```csharp
+ // Define the client interface
+ public interface IChatClient
+ {
+ Task ReceiveMessage(string user, string message);
+ Task UserJoined(string user);
+ Task UserLeft(string user);
+ }
+
+ // Implement the strongly-typed hub
+ public class ChatHub : Hub<IChatClient>
+ {
+ public async Task SendMessage(string user, string message)
+ {
+ // Compiler checks client method calls
+ await Clients.All.ReceiveMessage(user, message);
+ }
+
+ public override async Task OnConnectedAsync()
+ {
+ await Clients.Others.UserJoined(Context.User?.Identity?.Name ?? "Anonymous");
+ await base.OnConnectedAsync();
+ }
+
+ public override async Task OnDisconnectedAsync(Exception? exception)
+ {
+ await Clients.Others.UserLeft(Context.User?.Identity?.Name ?? "Anonymous");
+ await base.OnDisconnectedAsync(exception);
+ }
+ }
+ ```
+
+ ### Using Groups for Targeted Messaging
+ ```csharp
+ public class NotificationHub : Hub<INotificationClient>
+ {
+ public async Task JoinGroup(string groupName)
+ {
+ await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
+ await Clients.Group(groupName).UserJoined(Context.User?.Identity?.Name);
+ }
+
+ public async Task LeaveGroup(string groupName)
+ {
+ await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
+ }
+
+ public async Task SendToGroup(string groupName, string message)
+ {
+ await Clients.Group(groupName).ReceiveNotification(message);
+ }
+ }
+ ```
+
+ ### Hub Method with Custom Object Parameters (API Versioning)
+ ```csharp
+ // Use custom objects to avoid breaking changes
+ public class SendMessageRequest
+ {
+ public string Message { get; set; } = string.Empty;
+ public string? Recipient { get; set; } // Added later without breaking clients
+ public int? Priority { get; set; } // Added later without breaking clients
+ }
+
+ public class ChatHub : Hub<IChatClient>
+ {
+ public async Task SendMessage(SendMessageRequest request)
+ {
+ // Handle both old and new clients
+ if (request.Recipient != null)
+ {
+ await Clients.User(request.Recipient).ReceiveMessage(request.Message);
+ }
+ else
+ {
+ await Clients.All.ReceiveMessage(request.Message);
+ }
+ }
+ }
+ ```
+
+ ## Client Patterns
+
+ ### JavaScript Client with Automatic Reconnection
+ ```javascript
+ const connection = new signalR.HubConnectionBuilder()
+ .withUrl("/chatHub")
+ .withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) // Retry delays
+ .configureLogging(signalR.LogLevel.Information)
+ .build();
+
+ // Handle reconnection events
+ connection.onreconnecting(error => {
+ console.log("Reconnecting...", error);
+ updateUIForReconnecting();
+ });
+
+ connection.onreconnected(connectionId => {
+ console.log("Reconnected with ID:", connectionId);
+ // Rejoin groups - reconnection does not restore group membership
+ rejoinGroups();
+ updateUIForConnected();
+ });
+
+ connection.onclose(error => {
+ console.log("Connection closed", error);
+ updateUIForDisconnected();
+ });
+
+ async function start() {
+ try {
+ await connection.start();
+ console.log("SignalR Connected");
+ } catch (err) {
+ console.log(err);
+ setTimeout(start, 5000);
+ }
+ }
+
+ start();
+ ```
+
+ ### .NET Client with Reconnection
+ ```csharp
+ var connection = new HubConnectionBuilder()
+ .WithUrl("https://localhost:5001/chatHub", options =>
+ {
+ options.AccessTokenProvider = () => Task.FromResult(GetAccessToken());
+ })
+ .WithAutomaticReconnect()
+ .Build();
+
+ connection.Reconnecting += error =>
+ {
+ _logger.LogWarning("Connection lost. Reconnecting: {Error}", error?.Message);
+ return Task.CompletedTask;
+ };
+
+ connection.Reconnected += connectionId =>
+ {
+ _logger.LogInformation("Reconnected with ID: {ConnectionId}", connectionId);
+ // Rejoin groups after reconnection
+ return RejoinGroupsAsync();
+ };
+
+ connection.Closed += async error =>
+ {
+ _logger.LogError("Connection closed: {Error}", error?.Message);
+ await Task.Delay(Random.Shared.Next(0, 5) * 1000);
+ await connection.StartAsync();
+ };
+
+ await connection.StartAsync();
+ ```
+
+ ## Server Configuration
+
+ ### Hub Registration with Authentication
+ ```csharp
+ var builder = WebApplication.CreateBuilder(args);
+
+ builder.Services.AddSignalR(options =>
+ {
+ options.EnableDetailedErrors = builder.Environment.IsDevelopment();
+ options.MaximumReceiveMessageSize = 64 * 1024; // 64 KB
+ options.StreamBufferCapacity = 10;
+ options.KeepAliveInterval = TimeSpan.FromSeconds(15);
+ options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
+ })
+ .AddMessagePackProtocol(); // Binary protocol for performance
+
+ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
+ .AddJwtBearer(options =>
+ {
+ options.Events = new JwtBearerEvents
+ {
+ OnMessageReceived = context =>
+ {
+ // Read token from query string for WebSocket connections
+ var accessToken = context.Request.Query["access_token"];
+ var path = context.HttpContext.Request.Path;
+ if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs"))
+ {
+ context.Token = accessToken;
+ }
+ return Task.CompletedTask;
+ }
+ };
+ });
+
+ var app = builder.Build();
+
+ app.UseAuthentication();
+ app.UseAuthorization();
+
+ app.MapHub<ChatHub>("/hubs/chat");
+ ```
+
+ ### Sending Messages from Outside a Hub
+ ```csharp
+ public class NotificationService
+ {
+ private readonly IHubContext<NotificationHub, INotificationClient> _hubContext;
+
+ public NotificationService(IHubContext<NotificationHub, INotificationClient> hubContext)
+ {
+ _hubContext = hubContext;
+ }
+
+ public async Task NotifyAllAsync(string message)
+ {
+ await _hubContext.Clients.All.ReceiveNotification(message);
+ }
+
+ public async Task NotifyUserAsync(string userId, string message)
+ {
+ await _hubContext.Clients.User(userId).ReceiveNotification(message);
+ }
+
+ public async Task NotifyGroupAsync(string groupName, string message)
+ {
+ await _hubContext.Clients.Group(groupName).ReceiveNotification(message);
+ }
+ }
+ ```
+
+ ## Scaling with Redis Backplane
+
+ ```csharp
+ builder.Services.AddSignalR()
+ .AddStackExchangeRedis(connectionString, options =>
+ {
+ options.Configuration.ChannelPrefix = RedisChannel.Literal("MyApp");
+ });
+ ```
+
+ ## Anti-Patterns to Avoid
+
+ | Anti-Pattern | Why It's Bad | Better Approach |
+ |--------------|--------------|-----------------|
+ | Storing state in Hub properties | Hub instances are created per method call | Use `IMemoryCache`, database, or external store |
+ | Instantiating Hub directly | Bypasses SignalR infrastructure | Use `IHubContext<THub>` for external messaging |
+ | Not awaiting `SendAsync` calls | Messages may not be sent before hub method completes | Always `await` async hub calls |
+ | Adding method parameters without versioning | Breaking change for existing clients | Use custom object parameters |
+ | Ignoring reconnection group loss | Clients lose group membership on reconnect | Re-add to groups in `OnConnectedAsync` or client reconnect handler |
+ | Large payloads over SignalR | Memory pressure, bandwidth issues | Use REST/gRPC for bulk data, SignalR for notifications |
+ | Missing backplane in multi-server | Messages only reach clients on same server | Use Redis backplane or Azure SignalR Service |
+ | Exposing ORM entities directly | May serialize sensitive data | Use DTOs with explicit properties |
+ | Not validating incoming messages | Security risk after initial auth | Validate every hub method input |
+
+ ## Best Practices
+
+ ### Connection Management
+ 1. **Enable automatic reconnection** with exponential backoff delays
+ 2. **Handle group rejoining** explicitly after reconnection (connection ID changes)
+ 3. **Implement heartbeat monitoring** on the client to detect stale connections
+ 4. **Use sticky sessions** when scaling across multiple servers (unless using Azure SignalR Service)
+
+ ### Performance
+ 1. **Use MessagePack protocol** for smaller message sizes and faster serialization
+ 2. **Throttle high-frequency events** like typing indicators or mouse movements
+ 3. **Batch messages** when possible instead of many small sends
+ 4. **Set appropriate buffer sizes** based on expected message throughput
+
+ ### Security
+ 1. **Authenticate at connection time** using JWT tokens via query string
+ 2. **Authorize hub methods** using `[Authorize]` attribute
+ 3. **Validate all incoming messages** even after authentication
+ 4. **Use HTTPS** for all SignalR connections
+
+ ### API Design
+ 1. **Use strongly-typed hubs** to catch client method name typos at compile time
+ 2. **Use custom object parameters** to enable backward-compatible API evolution
+ 3. **Version hub names** (e.g., `ChatHubV2`) for breaking changes
+ 4. **Keep hub methods thin** and delegate business logic to services
+
+ ### Observability
+ 1. **Log connection events** (connect, disconnect, reconnect)
+ 2. **Track transport type** used by each connection
+ 3. **Monitor message delivery** latency and failure rates
+ 4. **Integrate with Application Insights** or other APM tools
+
## Deliver
- clear hub contracts and connection behavior
- real-time delivery that matches the product scenario
- validation for reconnection and authorization flows
+ - appropriate scale-out strategy for multi-server deployments
## Validate
- SignalR is the correct transport for the use case
- hub methods remain orchestration-oriented
- group and auth behavior are explicit and tested
+ - reconnection and group membership are handled correctly
+ - backplane is configured for multi-server scenarios
+ - message validation is implemented in hub methods