SignalR for real-time communication in Blazor apps including hubs, groups, streaming, and scaling
From dotnet-blazornpx claudepluginhub markus41/claude --plugin dotnet-blazorThis skill is limited to using the following tools:
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Migrates code, prompts, and API calls from Claude Sonnet 4.0/4.5 or Opus 4.1 to Opus 4.5, updating model strings on Anthropic, AWS, GCP, Azure platforms.
Analyzes BMad project state from catalog CSV, configs, artifacts, and query to recommend next skills or answer questions. Useful for help requests, 'what next', or starting BMad.
public sealed class NotificationHub : Hub
{
public async Task SendMessage(string user, string message)
{
await Clients.All.SendAsync("ReceiveMessage", user, message);
}
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).SendAsync("UserJoined", Context.UserIdentifier);
}
public async Task SendToGroup(string groupName, string message)
{
await Clients.Group(groupName).SendAsync("ReceiveMessage", message);
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
// Track connection
}
}
builder.Services.AddSignalR()
.AddJsonProtocol(options =>
{
options.PayloadSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
app.MapHub<NotificationHub>("/hubs/notifications");
Blazor Server already uses SignalR for its circuit. For additional hubs:
@rendermode InteractiveServer
@inject NavigationManager Navigation
@implements IAsyncDisposable
@code {
private HubConnection? _hub;
protected override async Task OnInitializedAsync()
{
_hub = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/hubs/notifications"))
.WithAutomaticReconnect()
.Build();
_hub.On<string, string>("ReceiveMessage", (user, message) =>
{
_messages.Add(new(user, message));
InvokeAsync(StateHasChanged);
});
await _hub.StartAsync();
}
public async ValueTask DisposeAsync()
{
if (_hub is not null)
await _hub.DisposeAsync();
}
}
public interface INotificationClient
{
Task ReceiveMessage(string user, string message);
Task UserJoined(string userId);
Task OrderUpdated(OrderStatusDto status);
}
public sealed class NotificationHub : Hub<INotificationClient>
{
public async Task SendMessage(string user, string message)
{
await Clients.All.ReceiveMessage(user, message);
}
}
public sealed class OrderService(IHubContext<NotificationHub, INotificationClient> hub)
{
public async Task UpdateStatusAsync(int orderId, string status)
{
// Update DB...
await hub.Clients.Group($"order-{orderId}")
.OrderUpdated(new OrderStatusDto(orderId, status));
}
}
builder.Services.AddSignalR()
.AddStackExchangeRedis(builder.Configuration.GetConnectionString("Redis")!);
public class ChatHub : Hub
{
// Broadcast to ALL connected clients
await Clients.All.SendAsync("ReceiveMessage", user, message);
// Send to specific client by connection ID
await Clients.Client(targetConnectionId).SendAsync("ReceivePrivateMessage", message);
// Send to all EXCEPT caller
await Clients.Others.SendAsync("ReceiveMessage", message);
// Group operations
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
await Clients.Group(groupName).SendAsync("ReceiveGroupMessage", message);
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
// Send to group except caller
await Clients.GroupExcept(groupName, Context.ConnectionId)
.SendAsync("ReceiveMessage", message);
}
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.withAutomaticReconnect()
.build();
// Server calls this method on client
connection.on("ReceiveMessage", (user, message) => {
document.getElementById("messages").innerHTML += `<li>${user}: ${message}</li>`;
});
// Client calls server method
await connection.invoke("SendMessage", user, message);
// Connection lifecycle
connection.onreconnecting((error) => console.log("Reconnecting..."));
connection.onreconnected((connectionId) => console.log("Reconnected:", connectionId));
connection.onclose((error) => console.log("Connection closed"));
connection.start().catch(err => console.error(err));
SignalR automatically negotiates the best transport: