
signalr-integration
SignalR integration patterns for real-time communication in ASP.NET Core Razor Pages applications. Use when implementing real-time features in ASP.NET Core applications, setting up SignalR hubs and clients, or managing WebSocket connections and groups.
- Installs
- 100
- Last 24h
- —
- Trending rank
- —
- Security
- Not audited
Install
Works with every agent the skills CLI supports.
npx skills add https://github.com/wshaddix/dotnet-skills --skill signalr-integration- 01
Install
Run the command in your project. The CLI asks which agents to install for.
- 02
Describe the task
Ask your agent for something this skill covers. It loads signalr-integration automatically when relevant.
- 03
Review the result
The agent follows the SKILL.md steps, scripts and references bundled with the skill.
SKILL.md
The instructions your agent reads when the skill is triggered.
Rationale
Real-time communication enhances user experience with instant updates, notifications, and collaborative features. SignalR provides a robust framework for bidirectional communication between server and clients. Without proper patterns, applications can suffer from connection leaks, scalability issues, and security vulnerabilities. These patterns provide production-ready approaches to SignalR in Razor Pages applications.
Patterns
Pattern 1: Hub Structure and Organization
Organize hubs by domain with proper authentication and group management.
// Base hub with common functionality
public abstract class AuthenticatedHub : Hub
{
protected string? UserId => Context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
protected bool IsAuthenticated => !string.IsNullOrEmpty(UserId);
public override async Task OnConnectedAsync()
{
if (!IsAuthenticated)
{
Context.Abort();
return;
}
await base.OnConnectedAsync();
}
}
// Notification hub for real-time updates
public interface INotificationClient
{
Task ReceiveNotification(NotificationMessage message);
Task NotificationRead(string notificationId);
Task UnreadCountUpdated(int count);
}
public class NotificationHub : AuthenticatedHub<INotificationClient>
{
private readonly INotificationService _notificationService;
private readonly ILogger<NotificationHub> _logger;
public NotificationHub(
INotificationService notificationService,
ILogger<NotificationHub> logger)
{
_notificationService = notificationService;
_logger = logger;
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
if (UserId != null)
{
// Join user-specific group
await Groups.AddToGroupAsync(Context.ConnectionId, $"user:{UserId}");
// Send initial unread count
var count = await _notificationService.GetUnreadCountAsync(UserId);
await Clients.Caller.UnreadCountUpdated(count);
_logger.LogDebug("User {UserId} connected to notification hub", UserId);
}
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
if (UserId != null)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"user:{UserId}");
_logger.LogDebug("User {UserId} disconnected from notification hub", UserId);
}
await base.OnDisconnectedAsync(exception);
}
public async Task MarkAsRead(string notificationId)
{
if (UserId == null) return;
await _notificationService.MarkAsReadAsync(UserId, notificationId);
var count = await _notificationService.GetUnreadCountAsync(UserId);
await Clients.Caller.UnreadCountUpdated(count);
}
public async Task SubscribeToTopic(string topic)
{
// Validate topic access
if (!await CanSubscribeToTopic(topic))
{
throw new HubException("Not authorized for this topic");
}
await Groups.AddToGroupAsync(Context.ConnectionId, $"topic:{topic}");
}
private Task<bool> CanSubscribeToTopic(string topic)
{
// Implement topic authorization logic
return Task.FromResult(true);
}
}
// Order status hub for real-time order updates
public interface IOrderClient
{
Task OrderStatusUpdated(string orderId, OrderStatus status, string? message);
Task OrderProgressUpdated(string orderId, int progressPercent);
Task OrderCompleted(string orderId);
}
public class OrderHub : AuthenticatedHub<IOrderClient>
{
private readonly IOrderService _orderService;
public OrderHub(IOrderService orderService)
{
_orderService = orderService;
}
public async Task SubscribeToOrder(string orderId)
{
if (UserId == null) return;
// Verify user owns this order
var order = await _orderService.GetOrderAsync(orderId);
if (order?.UserId != UserId)
{
throw new HubException("Not authorized to view this order");
}
await Groups.AddToGroupAsync(Context.ConnectionId, $"order:{orderId}");
}
public async Task UnsubscribeFromOrder(string orderId)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, $"order:{orderId}");
}
}
Pattern 2: Razor Pages Integration
Integrate SignalR clients in Razor Pages with proper connection lifecycle management.
// SignalR configuration in Program.cs
builder.Services.AddSignalR()
.AddJsonProtocol(options =>
{
options.PayloadSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
})
.AddStackExchangeRedis("redis:6379"); // For scale-out
// Authentication for SignalR
builder.Services.AddAuthentication()
.AddCookie(options =>
{
// Allow SignalR to use cookie auth
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
// Hub endpoints
app.MapHub<NotificationHub>("/hubs/notifications")
.RequireAuthorization();
app.MapHub<OrderHub>("/hubs/orders")
.RequireAuthorization();
// wwwroot/js/signalr-client.js
class SignalRClient {
constructor() {
this.connections = new Map();
this.reconnectDelays = [0, 2000, 5000, 10000, 30000];
}
async connect(hubUrl, hubName) {
if (this.connections.has(hubName)) {
return this.connections.get(hubName);
}
const connection = new signalR.HubConnectionBuilder()
.withUrl(hubUrl, {
transport: signalR.HttpTransportType.WebSockets |
signalR.HttpTransportType.ServerSentEvents
})
.withAutomaticReconnect(this.reconnectDelays)
.configureLogging(signalR.LogLevel.Information)
.build();
connection.onreconnecting(error => {
console.log(`Reconnecting to ${hubName}...`, error);
this.showReconnectingUI(hubName);
});
connection.onreconnected(connectionId => {
console.log(`Reconnected to ${hubName}`, connectionId);
this.hideReconnectingUI(hubName);
});
connection.onclose(error => {
console.log(`Connection to ${hubName} closed`, error);
this.connections.delete(hubName);
this.showDisconnectedUI(hubName);
});
try {
await connection.start();
this.connections.set(hubName, connection);
console.log(`Connected to ${hubName}`);
return connection;
} catch (err) {
console.error(`Failed to connect to ${hubName}`, err);
throw err;
}
}
async disconnect(hubName) {
const connection = this.connections.get(hubName);
if (connection) {
await connection.stop();
this.connections.delete(hubName);
}
}
getConnection(hubName) {
return this.connections.get(hubName);
}
showReconnectingUI(hubName) {
document.body.classList.add('signalr-reconnecting');
}
hideReconnectingUI(hubName) {
document.body.classList.remove('signalr-reconnecting');
}
showDisconnectedUI(hubName) {
document.body.classList.add('signalr-disconnected');
}
}
// Global instance
window.signalRClient = new SignalRClient();
// Razor Page with SignalR integration
public class OrderStatusModel : PageModel
{
private readonly IOrderService _orderService;
public OrderStatusModel(IOrderService orderService)
{
_orderService = orderService;
}
public Order Order { get; set; } = null!;
public async Task<IActionResult> OnGetAsync(string orderId)
{
Order = await _orderService.GetOrderAsync(orderId);
if (Order == null || Order.UserId != User.FindFirstValue(ClaimTypes.NameIdentifier))
{
return NotFound();
}
return Page();
}
}
<!-- OrderStatus.cshtml -->
@page "{orderId}"
@model OrderStatusModel
@inject IConfiguration Configuration
@section Scripts {
<script src="~/lib/microsoft/signalr/dist/browser/signalr.min.js"></script>
<script src="~/js/signalr-client.js"></script>
<script>
document.addEventListener('DOMContentLoaded', async function() {
const orderId = '@Model.Order.Id';
try {
const connection = await window.signalRClient.connect(
'/hubs/orders',
'orders'
);
// Subscribe to order updates
await connection.invoke('SubscribeToOrder', orderId);
// Handle status updates
connection.on('OrderStatusUpdated', (id, status, message) => {
if (id === orderId) {
updateStatusUI(status, message);
}
});
connection.on('OrderProgressUpdated', (id, progress) => {
if (id === orderId) {
updateProgressBar(progress);
}
});
connection.on('OrderCompleted', (id) => {
if (id === orderId) {
showCompletionUI();
}
});
} catch (err) {
console.error('Failed to connect:', err);
showFallbackUI();
}
});
function updateStatusUI(status, message) {
document.getElementById('status-badge').textContent = status;
if (message) {
document.getElementById('status-message').textContent = message;
}
}
function updateProgressBar(progress) {
const bar = document.getElementById('progress-bar');
bar.style.width = progress + '%';
bar.textContent = progress + '%';
}
function showCompletionUI() {
document.getElementById('completion-modal').classList.remove('hidden');
}
function showFallbackUI() {
// Fall back to polling
setInterval(() => location.reload(), 30000);
}
</script>
}
<div class="order-status">
<h1>Order #@Model.Order.Id</h1>
<span id="status-badge" class="badge">@Model.Order.Status</span>
<p id="status-message" class="text-muted"></p>
<div class="progress">
<div id="progress-bar" class="progress-bar" style="width: 0%">0%</div>
</div>
</div>
<div id="completion-modal" class="modal hidden">
<div class="modal-content">
<h2>Order Complete!</h2>
<p>Your order has been processed successfully.</p>
</div>
</div>
Pattern 3: Server-Side Broadcasting
Send notifications from services and background workers to connected clients.
// Notification service that broadcasts to clients
public interface IRealTimeNotificationService
{
Task SendToUserAsync(string userId, NotificationMessage message);
Task SendToUsersAsync(IEnumerable<string> userIds, NotificationMessage message);
Task BroadcastToTopicAsync(string topic, NotificationMessage message);
Task SendToGroupAsync(string groupName, NotificationMessage message);
}
public class SignalRNotificationService : IRealTimeNotificationService
{
private readonly IHubContext<NotificationHub, INotificationClient> _hubContext;
private readonly ILogger<SignalRNotificationService> _logger;
public SignalRNotificationService(
IHubContext<NotificationHub, INotificationClient> hubContext,
ILogger<SignalRNotificationService> logger)
{
_hubContext = hubContext;
_logger = logger;
}
public async Task SendToUserAsync(string userId, NotificationMessage message)
{
try
{
await _hubContext.Clients.Group($"user:{userId}")
.ReceiveNotification(message);
_logger.LogDebug("Notification sent to user {UserId}", userId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send notification to user {UserId}", userId);
}
}
public async Task SendToUsersAsync(IEnumerable<string> userIds, NotificationMessage message)
{
var tasks = userIds.Select(userId => SendToUserAsync(userId, message));
await Task.WhenAll(tasks);
}
public async Task BroadcastToTopicAsync(string topic, NotificationMessage message)
{
await _hubContext.Clients.Group($"topic:{topic}")
.ReceiveNotification(message);
}
public async Task SendToGroupAsync(string groupName, NotificationMessage message)
{
await _hubContext.Clients.Group(groupName)
.ReceiveNotification(message);
}
public async Task UpdateUnreadCountAsync(string userId, int count)
{
await _hubContext.Clients.Group($"user:{userId}")
.UnreadCountUpdated(count);
}
}
// Background service that sends notifications
public class OrderProcessingNotifier : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<OrderProcessingNotifier> _logger;
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
await using var scope = _serviceProvider.CreateAsyncScope();
var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();
var notificationService = scope.ServiceProvider.GetRequiredService<IRealTimeNotificationService>();
// Get orders that need status updates
var ordersToUpdate = await orderService.GetOrdersNeedingUpdatesAsync(stoppingToken);
foreach (var order in ordersToUpdate)
{
await notificationService.SendToUserAsync(
order.UserId,
new NotificationMessage
{
Id = Guid.NewGuid().ToString(),
Title = "Order Update",
Message = $"Your order #{order.Id} status changed to {order.Status}",
Type = NotificationType.OrderUpdate,
Data = new Dictionary<string, object>
{
["orderId"] = order.Id,
["status"] = order.Status
}
});
await notificationService.UpdateUnreadCountAsync(
order.UserId,
await orderService.GetUnreadNotificationCountAsync(order.UserId));
}
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in order processing notifier");
await Task.Delay(TimeSpan.FromSeconds(60), stoppingToken);
}
}
}
}
Pattern 4: Connection Management and Scale-Out
Handle connection scaling with Redis backplane and connection state management.
// Connection tracking service
public interface IConnectionTracker
{
Task AddConnectionAsync(string userId, string connectionId);
Task RemoveConnectionAsync(string userId, string connectionId);
Task<IReadOnlyList<string>> GetUserConnectionsAsync(string userId);
Task<bool> IsUserOnlineAsync(string userId);
}
public class RedisConnectionTracker : IConnectionTracker
{
private readonly IDistributedCache _cache;
private readonly TimeSpan _connectionTimeout = TimeSpan.FromHours(2);
public RedisConnectionTracker(IDistributedCache cache)
{
_cache = cache;
}
public async Task AddConnectionAsync(string userId, string connectionId)
{
var key = $"connections:{userId}";
var connections = await GetConnectionsAsync(userId);
connections.Add(connectionId);
await _cache.SetStringAsync(key,
JsonSerializer.Serialize(connections),
new DistributedCacheEntryOptions
{
SlidingExpiration = _connectionTimeout
});
}
public async Task RemoveConnectionAsync(string userId, string connectionId)
{
var key = $"connections:{userId}";
var connections = await GetConnectionsAsync(userId);
connections.Remove(connectionId);
if (connections.Count == 0)
{
await _cache.RemoveAsync(key);
}
else
{
await _cache.SetStringAsync(key,
JsonSerializer.Serialize(connections),
new DistributedCacheEntryOptions
{
SlidingExpiration = _connectionTimeout
});
}
}
public async Task<IReadOnlyList<string>> GetUserConnectionsAsync(string userId)
{
return await GetConnectionsAsync(userId);
}
public async Task<bool> IsUserOnlineAsync(string userId)
{
var connections = await GetConnectionsAsync(userId);
return connections.Count > 0;
}
private async Task<List<string>> GetConnectionsAsync(string userId)
{
var key = $"connections:{userId}";
var json = await _cache.GetStringAsync(key);
return string.IsNullOrEmpty(json)
? new List<string>()
: JsonSerializer.Deserialize<List<string>>(json) ?? new List<string>();
}
}
// Scale-out configuration
builder.Services.AddSignalR()
.AddStackExchangeRedis(options =>
{
options.Configuration.ConnectionString = "redis:6379";
options.Configuration.AbortOnConnectFail = false;
})
.AddMessagePackProtocol(); // Binary protocol for better performance
// Sticky sessions are NOT needed with Redis backplane
// Load balancer can use round-robin
Anti-Patterns
// ❌ BAD: Creating new connection on every page
const connection = new signalR.HubConnectionBuilder()
.withUrl('/hubs/notifications')
.build();
await connection.start(); // Creates new connection every time!
// ✅ GOOD: Reuse connections across pages
// Use the SignalRClient class shown in Pattern 2
// ❌ BAD: Calling clients without error handling
public async Task SendNotification(string userId, string message)
{
await _hubContext.Clients.Group($"user:{userId}")
.ReceiveNotification(message); // May throw if user not connected
}
// ✅ GOOD: Handle errors gracefully
public async Task SendNotification(string userId, string message)
{
try
{
await _hubContext.Clients.Group($"user:{userId}")
.ReceiveNotification(message);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to send notification to {UserId}", userId);
// Queue for later delivery or mark as pending
}
}
// ❌ BAD: Storing hub context in static/singleton
public static class BadNotificationService
{
public static IHubContext<NotificationHub> HubContext { get; set; } = null!;
}
// ✅ GOOD: Inject IHubContext where needed
public class GoodNotificationService
{
private readonly IHubContext<NotificationHub> _hubContext;
public GoodNotificationService(IHubContext<NotificationHub> hubContext)
{
_hubContext = hubContext;
}
}
// ❌ BAD: Blocking in hub methods
public string GetData()
{
var result = _service.GetDataAsync().Result; // Deadlock risk!
return result;
}
// ✅ GOOD: Use async throughout
public async Task<string> GetDataAsync()
{
return await _service.GetDataAsync();
}
// ❌ BAD: Not validating group membership
public async Task JoinGroup(string groupName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
// Anyone can join any group!
}
// ✅ GOOD: Validate before adding to group
public async Task JoinGroup(string groupName)
{
if (!await CanJoinGroup(groupName))
{
throw new HubException("Access denied");
}
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
}
// ❌ BAD: Trusting client-provided user ID
public async Task SendMessage(string userId, string message)
{
// Client could impersonate any user!
await Clients.Group($"user:{userId}").ReceiveMessage(message);
}
// ✅ GOOD: Use authenticated identity from Context
public async Task SendMessage(string targetUserId, string message)
{
var senderId = Context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(senderId))
{
throw new HubException("Not authenticated");
}
// Validate sender can message target
if (!await CanMessageUser(senderId, targetUserId))
{
throw new HubException("Not authorized");
}
await Clients.Group($"user:{targetUserId}").ReceiveMessage(new {
From = senderId,
Message = message,
Timestamp = DateTime.UtcNow
});
}
// ❌ BAD: Broadcasting to all clients without filtering
public async Task BroadcastUpdate(string data)
{
await Clients.All.ReceiveUpdate(data); // All connected clients!
}
// ✅ GOOD: Target specific groups or use authorization
public async Task BroadcastToAuthorized(string data)
{
// Only send to users with specific role or permission
await Clients.Group("admins").ReceiveUpdate(data);
}
References
FAQ
What does the signalr-integration skill do?
SignalR integration patterns for real-time communication in ASP.NET Core Razor Pages applications. Use when implementing real-time features in ASP.NET Core applications, setting up SignalR hubs and clients, or managing WebSocket connections and groups.
How do I install signalr-integration?
Run npx skills add https://github.com/wshaddix/dotnet-skills --skill signalr-integration in your project. The skills CLI detects the agents you use and copies the skill into the right folder. For Claude Code you can also place the skill folder at ~/.claude/skills/signalr-integration/ (personal) or .claude/skills/signalr-integration/ (project).
Which AI agents can use signalr-integration?
Any agent that supports the open Agent Skills format (a folder with a SKILL.md file), including Claude Code, Codex, Cursor, GitHub Copilot, Gemini CLI, Windsurf, Cline, OpenCode and more.
Is signalr-integration safe to install?
No partner security audit is available yet. Skills can run commands and scripts, so read the SKILL.md and any bundled files before installing.
Who maintains signalr-integration?
It is published by wshaddix in wshaddix/dotnet-skills. It has 100 recorded installs.
More from wshaddix/dotnet-skills
dotnet-xunit
wshaddix/dotnet-skills
Writing xUnit tests. v3 Fact/Theory, fixtures, parallelism, IAsyncLifetime, v2 compatibility.
dotnet-testing-strategy
wshaddix/dotnet-skills
Deciding how to test .NET code. Unit vs integration vs E2E decision tree, test doubles.
modern-csharp-coding-standards
wshaddix/dotnet-skills
Write modern, high-performance C code using records, pattern matching, value objects, async/await, Span /Memory , and best-practice API design patterns. Emphasizes functional-style programming with C 12+ features. Use when writing new C code or refactoring existing code, designing public APIs for libraries or services, optimizing performance-critical code paths, or building async/await-heavy applications.
csharp-wolverinefx
wshaddix/dotnet-skills
Build .NET applications with WolverineFX for messaging, HTTP services, and event sourcing. Use when implementing command handlers, message handlers, HTTP endpoints with WolverineFx.HTTP, transactional outbox patterns, event sourcing with Marten, CQRS architectures, cascading messages, batch message processing, or configuring transports like RabbitMQ, Azure Service Bus, or Amazon SQS.
testcontainers
wshaddix/dotnet-skills
Patterns for using Testcontainers in .NET integration tests to spin up real dependencies like databases and message queues. Use when writing integration tests that require real databases, testing with message brokers like RabbitMQ or Kafka, or isolating test dependencies with Docker containers.
microsoft-extensions-dependency-injection
wshaddix/dotnet-skills
Dependency injection patterns and best practices using Microsoft.Extensions.DependencyInjection for .NET applications. Use when configuring DI containers in .NET, choosing between service lifetimes (Singleton, Scoped, Transient), or implementing decorator patterns and service interception.
Popular backend & apis skills
find-skills
vercel-labs/skills
Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
entra-app-registration
microsoft/azure-skills
Guides Microsoft Entra ID app registration, OAuth 2.0 authentication, and MSAL integration. USE FOR: create app registration, register Azure AD app, configure OAuth, set up authentication, add API permissions, generate service principal, MSAL example, console app auth, Entra ID setup, Azure AD authentication. DO NOT USE FOR: Key Vault secrets (use azure-keyvault-expiration-audit), general Azure resource security guidance.
google-agents-cli-adk-code
google/agents-cli
This skill should be used when the user wants to "write agent code", "build an agent with ADK", "add a tool", "create a callback", "define an agent", "use state management" — in a project that needs ADK (Agent Development Kit) API patterns and code examples. It provides a quick reference for agent types, tool definitions, orchestration patterns, callbacks, state management, the graph Workflow API, and reference recipes to study. Do NOT use for scaffolding (use google-agents-cli-scaffold) or deployment (use google-agents-cli-deploy).
embedded-captions
heygen-com/hyperframes
Add captions or subtitles to an existing single-subject talking-head video without editing the footage. Use for plain verbatim captions, cinematic captions embedded behind the subject, VFX captions, “炸/特效/酷炫字幕,” or a named identity from the 35-style catalog. Route by visual identity, not by backend engine. The quiet anchor rail is the default; embed every word only when the user explicitly wants a fully cinematic treatment. The workflow runs locally end to end, including transcription and subject matting; split multi-shot footage before applying it.
entra-agent-id
microsoft/azure-skills
Provision Microsoft Entra Agent Identity Blueprints, BlueprintPrincipals, and per-instance Agent Identities via Microsoft Graph, and configure OAuth 2.0 token exchange (fmipath, OBO, cross-tenant) including the Microsoft Entra SDK for AgentID sidecar. USE FOR: Agent Identity Blueprint, BlueprintPrincipal, agent OAuth, fmipath token exchange, agent OBO, Workload Identity Federation for agents, polyglot agent auth, Microsoft.Identity.Web.AgentIdentities. DO NOT USE FOR: standard Entra app registration (use entra-app-registration), Microsoft Foundry agent authoring (use microsoft-foundry).
neon
neondatabase/agent-skills
Overview of Neon, a complete set of cloud backend primitives around Lakebase Postgres: Auth, Object Storage, Functions, the AI Gateway, and Realtime. Start here to choose Neon for undecided login, files, APIs, and LLM calls, set up the CLI or MCP server, and follow the branch-first workflow. Use when building an app or backend on Neon, or when "Neon" or "Lakebase Postgres" is mentioned. Child skill neon-postgres wins for an existing DATABASEURL, SQL, schema, inspect, or search. Child skill neon-auth wins for login, users, sessions, identity routing, and Managed Better Auth setup. Child skill neon-realtime wins for live queries in reactive apps and agents. Also use for object storage, S3, buckets, serverless functions, function triggers, cron, AI gateway, LLM calls, realtime, logs, Loki, Grafana, observability, postgres, database, backend, Claimable Neon, neon.new, or a no-signup database.