Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Implement or review SignalR hubs, streaming, reconnection, transport, and real-time delivery patterns in ASP.NET Core applications. USE FOR: building chat, notification, collaboration, or live-update features; debugging hub lifetime, connection state, or transport issues; deciding whether SignalR or another. DO NOT USE FOR: unrelated stacks; generic tasks that do not need this specific guidance. INVOKES: inspect the repository context, edit targeted files, and run relevant build, test, lint, or
.claude/skills/managedcode-signalr/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 165% | 0% |
| case-05 | ✓→✓ | = Same ✓ | 163% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 295% | 0% |
| case-02 | ✓→✓ | = Same ✓ | 151% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 159% | 0% |
dotnet/aspnetcore v10.0.11 is a servicing release and does not change the SignalR programming model. Keep guidance focused on hub contract design, reconnection, transport, authorization, and scale-out validation.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); } }
csharppublic 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); } }
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); } } }
javascriptconst 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();
csharpvar 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();
csharpvar 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");
csharppublic 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); } }
csharpbuilder.Services.AddSignalR() .AddStackExchangeRedis(connectionString, options => { options.Configuration.ChannelPrefix = RedisChannel.Literal("MyApp"); });
| 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 |
[Authorize] attributeChatHubV2) for breaking changesOther measured skills in the registry, with their headline benchmark lift.