Claude
Skills
Sign in
Back

signalr-integration

Included with Lifetime
$97 forever

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.

General

What this skill does


## 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.

```csharp
// 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.

```csharp
// 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();
```

```javascript
// 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();
```

```csharp
// 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 = aw
Files: 1
Size: 22.2 KB
Complexity: 27/100
Category: General

Related in General