Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SignalR

Overview Questions

  • SignalR là gì và khi nào cần sử dụng?
  • Hub là gì và cách tạo Hub?
  • Client-server communication hoạt động ra sao?
  • Groups và Users trong SignalR khác nhau như thế nào?
  • Scale SignalR với Redis backplane ra sao?

SignalR Basics

What is SignalR?

┌─────────────────────────────────────────────────────────────────┐
│                    SIGNALR ARCHITECTURE                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐                  │
│  │  Client  │◀──▶│   Hub    │◀──▶│  Client  │                  │
│  │  (Web)   │    │  (Server)│    │  (Web)   │                  │
│  └──────────┘    └──────────┘    └──────────┘                  │
│       │                                │                        │
│       │         ┌──────────┐           │                        │
│       └────────▶│  Client  │◀──────────┘                        │
│                 │  (Mobile)│                                     │
│                 └──────────┘                                     │
│                                                                 │
│  - Real-time communication                                      │
│  - Server push to clients                                       │
│  - WebSocket, Server-Sent Events, Long Polling                  │
│  - Chat, notifications, live updates                            │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Setup

dotnet add package Microsoft.AspNetCore.SignalR
// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR();

var app = builder.Build();

app.MapHub<ChatHub>("/chat");

app.Run();

Hub

Basic Hub

public class ChatHub : Hub
{
    // Client gọi method này
    public async Task SendMessage(string user, string message)
    {
        // Server gọi method trên tất cả clients
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

    // Gửi cho người gọi
    public async Task SendMessageToCaller(string user, string message)
    {
        await Clients.Caller.SendAsync("ReceiveMessage", user, message);
    }

    // Gửi cho người khác (trừ người gọi)
    public async Task SendMessageToOthers(string user, string message)
    {
        await Clients.Others.SendAsync("ReceiveMessage", user, message);
    }
}

Hub with Dependency Injection

public class ChatHub : Hub
{
    private readonly ILogger<ChatHub> _logger;
    private readonly IChatService _chatService;

    public ChatHub(ILogger<ChatHub> logger, IChatService chatService)
    {
        _logger = logger;
        _chatService = chatService;
    }

    public async Task SendMessage(string user, string message)
    {
        _logger.LogInformation("Message from {User}: {Message}", user, message);
        
        // Save to database
        await _chatService.SaveMessage(user, message);
        
        // Broadcast
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }
}

Client Communication

Server to Client

public class NotificationHub : Hub
{
    // Gửi cho tất cả
    public async Task Broadcast(string message)
    {
        await Clients.All.SendAsync("OnBroadcast", message);
    }

    // Gửi cho client cụ thể (theo ConnectionId)
    public async Task SendToClient(string connectionId, string message)
    {
        await Clients.Client(connectionId).SendAsync("OnMessage", message);
    }

    // Gửi cho nhóm
    public async Task SendToGroup(string group, string message)
    {
        await Clients.Group(group).SendAsync("OnMessage", message);
    }

    // Gửi cho user cụ thể (theo UserId)
    public async Task SendToUser(string userId, string message)
    {
        await Clients.User(userId).SendAsync("OnMessage", message);
    }
}

Client to Server

// JavaScript Client
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chat")
    .build();

// Receive message from server
connection.on("ReceiveMessage", (user, message) => {
    console.log(`${user}: ${message}`);
});

// Send message to server
connection.invoke("SendMessage", "John", "Hello!").catch(err => console.error(err));

// Start connection
connection.start().catch(err => console.error(err));

Groups

Group Management

public class ChatHub : Hub
{
    // Join group
    public async Task JoinGroup(string groupName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        
        await Clients.Group(groupName).SendAsync(
            "OnUserJoined", 
            $"{Context.ConnectionId} joined {groupName}");
    }

    // Leave group
    public async Task LeaveGroup(string groupName)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
        
        await Clients.Group(groupName).SendAsync(
            "OnUserLeft", 
            $"{Context.ConnectionId} left {groupName}");
    }

    // Send to group
    public async Task SendToGroup(string groupName, string message)
    {
        await Clients.Group(groupName).SendAsync("OnMessage", message);
    }
}

Group Lifecycle

public class ChatHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, "General");
        await Clients.Caller.SendAsync("OnConnected", Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, "General");
        await Clients.Others.SendAsync(
            "OnDisconnected", 
            Context.ConnectionId);
        await base.OnDisconnectedAsync(exception);
    }
}

Strongly-typed Hubs

Interface-based Hub

// Define client methods interface
public interface IChatClient
{
    Task ReceiveMessage(string user, string message);
    Task UserJoined(string user);
    Task UserLeft(string user);
}

// Strongly-typed Hub
public class ChatHub : Hub<IChatClient>
{
    public async Task SendMessage(string user, string message)
    {
        // Type-safe client calls
        await Clients.All.ReceiveMessage(user, message);
    }

    public async Task JoinGroup(string groupName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        await Clients.Group(groupName).UserJoined(Context.ConnectionId);
    }
}

Authentication & Authorization

Hub Authorization

// Program.cs
app.MapHub<ChatHub>("/chat", options =>
{
    options.Transports = HttpTransportType.WebSockets | 
                         HttpTransportType.ServerSentEvents;
});

// Hub với authorization
[Authorize]
public class ChatHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        var userId = Context.UserIdentifier; // From JWT claim
        var userName = Context.User?.Identity?.Name;
        
        await base.OnConnectedAsync();
    }
}

Scaling

Redis Backplane

dotnet add package Microsoft.AspNetCore.SignalR.StackExchangeRedis
builder.Services.AddSignalR()
    .AddStackExchangeRedis("localhost:6379");

Azure SignalR Service

dotnet add package Microsoft.Azure.SignalR
builder.Services.AddSignalR()
    .AddAzureSignalR(options =>
    {
        options.ConnectionString = 
            builder.Configuration["Azure:SignalR:ConnectionString"];
    });

// Map hub
app.MapHub<ChatHub>("/chat");

Best Practices

1. Handle Connection Lifecycle

// ✅ Good - handle connection events
public class ChatHub : Hub
{
    public override async Task OnConnectedAsync()
    {
        _logger.LogInformation("Client connected: {ConnectionId}", Context.ConnectionId);
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        _logger.LogInformation("Client disconnected: {ConnectionId}", Context.ConnectionId);
        await base.OnDisconnectedAsync(exception);
    }
}

2. Use Groups for Targeted Messages

// ✅ Good - use groups for targeted messages
public async Task SendToRoom(string roomId, string message)
{
    await Clients.Group($"room-{roomId}").SendAsync("OnMessage", message);
}

// ❌ Bad - send to all when only room needs it
public async Task SendToRoom(string roomId, string message)
{
    await Clients.All.SendAsync("OnMessage", message); // All clients receive!
}

3. Handle Reconnection

// JavaScript Client - handle reconnection
connection.onclose(async () => {
    await start();
});

async function start() {
    try {
        await connection.start();
        console.log("SignalR connected");
    } catch (err) {
        console.log("SignalR connection failed");
        setTimeout(() => start(), 5000);
    }
}