Dotnet-skills dotnet-signalr
Implement or review SignalR hubs, streaming, reconnection, transport, and real-time delivery patterns in ASP.NET Core applications.
install
source · Clone the upstream repo
git clone https://github.com/managedcode/dotnet-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/managedcode/dotnet-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/catalog/Frameworks/SignalR/skills/dotnet-signalr" ~/.claude/skills/managedcode-dotnet-skills-dotnet-signalr && rm -rf "$T"
manifest:
catalog/Frameworks/SignalR/skills/dotnet-signalr/SKILL.mdsource content
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
- SignalR Hubs
- SignalR API Design Considerations
- SignalR Production Hosting and Scaling
- SignalR Configuration
References
- patterns.md - Detailed hub patterns, streaming, groups, presence, and advanced messaging techniques
- anti-patterns.md - Common SignalR mistakes and how to avoid them
Workflow
- Use SignalR for broadcast-style or connection-oriented real-time features; do not force gRPC into hub-style fan-out scenarios.
- Model hub contracts intentionally and keep hub methods thin, delegating durable work elsewhere.
- Plan for reconnection, backpressure, auth, and fan-out costs instead of treating real-time messaging as stateless request/response.
- Use groups, presence, and connection metadata deliberately so scale-out behavior is understandable.
- If Native AOT or trimming is in play, validate supported protocols and serialization choices explicitly.
- Test connection behavior and failure modes, not just happy-path message delivery.
Hub Patterns
Strongly-Typed Hub (Recommended)
// 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
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)
// 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
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
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
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
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
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 , database, or external store |
| Instantiating Hub directly | Bypasses SignalR infrastructure | Use for external messaging |
Not awaiting calls | Messages may not be sent before hub method completes | Always 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 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
- Enable automatic reconnection with exponential backoff delays
- Handle group rejoining explicitly after reconnection (connection ID changes)
- Implement heartbeat monitoring on the client to detect stale connections
- Use sticky sessions when scaling across multiple servers (unless using Azure SignalR Service)
Performance
- Use MessagePack protocol for smaller message sizes and faster serialization
- Throttle high-frequency events like typing indicators or mouse movements
- Batch messages when possible instead of many small sends
- Set appropriate buffer sizes based on expected message throughput
Security
- Authenticate at connection time using JWT tokens via query string
- Authorize hub methods using
attribute[Authorize] - Validate all incoming messages even after authentication
- Use HTTPS for all SignalR connections
API Design
- Use strongly-typed hubs to catch client method name typos at compile time
- Use custom object parameters to enable backward-compatible API evolution
- Version hub names (e.g.,
) for breaking changesChatHubV2 - Keep hub methods thin and delegate business logic to services
Observability
- Log connection events (connect, disconnect, reconnect)
- Track transport type used by each connection
- Monitor message delivery latency and failure rates
- 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