SignalR trên .NET 10 — Real-Time Communication, Scale-Out và Notification Push cho Production

Posted on: 4/17/2026 9:10:35 PM

Trong thế giới web hiện đại, người dùng kỳ vọng mọi thứ phải tức thời — tin nhắn chat xuất hiện ngay lập tức, dashboard cập nhật real-time, notification push không cần refresh trang. SignalR trên ASP.NET Core là giải pháp production-ready để xây dựng các tính năng real-time này trên nền tảng .NET, với khả năng tự động chọn transport tối ưu, strongly-typed hub, và scale-out tới hàng triệu kết nối. Bài viết này sẽ đi sâu vào kiến trúc SignalR trên .NET 10, các pattern thực chiến, chiến lược scale-out với Redis Backplane và Azure SignalR Service, cùng so sánh với SSE và raw WebSocket.

1. SignalR — Real-Time Framework cho .NET

ASP.NET Core SignalR là một thư viện open-source giúp đơn giản hóa việc thêm tính năng real-time vào ứng dụng web. Thay vì phải tự quản lý WebSocket connections, heartbeat, reconnection và fallback, SignalR trừu tượng hóa toàn bộ complexity này thông qua khái niệm Hub — một high-level API cho phép server gọi method trên client và ngược lại.

<1ms Latency với WebSocket transport
3 Transport tự động: WS, SSE, Long Polling
1M+ Connections với Azure SignalR Service
0 Dòng code quản lý connection thủ công

1.1. Kiến trúc Hub — Trung tâm giao tiếp

Hub là khái niệm cốt lõi của SignalR. Nó hoạt động như một pipeline hai chiều giữa server và client. Server có thể gọi method trên bất kỳ client nào (hoặc nhóm client), và client có thể invoke method trên server — tất cả qua một connection duy nhất.

graph TB
    C1["Client 1
(Browser)"] -->|"WebSocket"| HUB["SignalR Hub
(ASP.NET Core)"] C2["Client 2
(Mobile)"] -->|"SSE Fallback"| HUB C3["Client 3
(Desktop)"] -->|"WebSocket"| HUB HUB -->|"SendAsync"| C1 HUB -->|"SendAsync"| C2 HUB -->|"SendAsync"| C3 HUB --> GROUPS["Groups
(room-123, admin)"] HUB --> USERS["Users
(userId mapping)"] GROUPS -->|"Group Broadcast"| C1 GROUPS -->|"Group Broadcast"| C3 style HUB fill:#e94560,stroke:#fff,color:#fff style C1 fill:#2c3e50,stroke:#e94560,color:#fff style C2 fill:#2c3e50,stroke:#e94560,color:#fff style C3 fill:#2c3e50,stroke:#e94560,color:#fff style GROUPS fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style USERS fill:#f8f9fa,stroke:#e94560,color:#2c3e50

Hình 1: Kiến trúc SignalR Hub — server và client giao tiếp hai chiều qua một connection duy nhất

// Định nghĩa Hub trên server — .NET 10
public class ChatHub : Hub
{
    public async Task SendMessage(string user, string message)
    {
        // Gửi tới TẤT CẢ clients đang kết nối
        await Clients.All.SendAsync("ReceiveMessage", user, message);
    }

    public async Task JoinRoom(string roomName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
        await Clients.Group(roomName)
            .SendAsync("UserJoined", Context.User?.Identity?.Name);
    }

    public override async Task OnConnectedAsync()
    {
        // Tự động trigger khi client kết nối
        await Clients.Caller.SendAsync("Connected", Context.ConnectionId);
        await base.OnConnectedAsync();
    }
}

1.2. Strongly-Typed Hub — Type Safety hai chiều

Một trong những điểm mạnh nhất của SignalR trên .NET là Strongly-Typed Hub. Thay vì dùng magic string "ReceiveMessage", bạn định nghĩa interface cho client methods — compiler sẽ kiểm tra tại build time, không còn runtime error do typo.

// Định nghĩa contract cho client methods
public interface IChatClient
{
    Task ReceiveMessage(string user, string message);
    Task UserJoined(string userName);
    Task UserLeft(string userName);
    Task TypingIndicator(string userName, bool isTyping);
}

// Strongly-Typed Hub — compiler kiểm tra mọi SendAsync call
public class ChatHub : Hub<IChatClient>
{
    public async Task SendMessage(string user, string message)
    {
        // Clients.All trả về IChatClient — có IntelliSense đầy đủ
        await Clients.All.ReceiveMessage(user, message);
    }

    public async Task StartTyping()
    {
        var userName = Context.User?.Identity?.Name ?? "Anonymous";
        await Clients.Others.TypingIndicator(userName, true);
    }
}

Tại sao nên dùng Strongly-Typed Hub?

Với Strongly-Typed Hub, nếu bạn đổi tên method ReceiveMessage thành OnMessageReceived trên interface mà quên sửa phía client JavaScript, compiler sẽ báo lỗi ngay. Điều này đặc biệt quan trọng trong dự án lớn với nhiều Hub methods — tránh hoàn toàn lỗi runtime do typo mà chỉ phát hiện khi test thủ công.

2. Transport Protocols — WebSocket, SSE và Long Polling

SignalR tự động đàm phán transport tốt nhất tại thời điểm kết nối. Quá trình này diễn ra trong hai bước: (1) client gửi POST /negotiate để lấy danh sách transport server hỗ trợ, (2) client chọn transport tốt nhất mà cả hai bên đều support.

sequenceDiagram
    participant C as Client
    participant S as Server
    C->>S: POST /chat/negotiate
    S-->>C: {connectionId, transports: [WS, SSE, LP]}
    alt WebSocket available
        C->>S: Upgrade: websocket
        S-->>C: 101 Switching Protocols
        Note over C,S: Full-duplex communication
    else SSE fallback
        C->>S: GET /chat?transport=SSE
        S-->>C: text/event-stream (server→client)
        C->>S: POST /chat (client→server)
    else Long Polling last resort
        C->>S: GET /chat?transport=LongPolling
        Note over C,S: Server giữ request cho đến khi có data
        S-->>C: Response with data
        C->>S: GET (poll tiếp)
    end

Hình 2: Quá trình transport negotiation — SignalR tự động chọn protocol tối ưu

TransportHướngLatencyBrowser SupportKhi nào dùng
WebSocketFull-duplexThấp nhất (~1ms)Mọi browser hiện đạiMặc định — chat, game, collaboration
Server-Sent EventsServer → ClientThấpTất cả (trừ IE)Dashboard, notification, progress bar
Long PollingGiả lập duplexCao (poll interval)Mọi browserFallback cuối cùng (corporate proxy)

2.1. Server-Sent Events trên .NET 10 — Cải tiến đáng kể

.NET 10 bổ sung Results.ServerSentEvents — API mới cho phép wrap IAsyncEnumerable<T> thành SSE response chuẩn, không cần thư viện bên ngoài. Kết hợp với Kestrel hỗ trợ HTTP/2, SSE trên .NET 10 giải quyết được vấn đề giới hạn 6 kết nối trên mỗi domain (HTTP/1.1) — nhiều SSE stream có thể chia sẻ một TCP connection qua HTTP/2 multiplexing.

// .NET 10 — SSE với Minimal API (không cần SignalR)
app.MapGet("/dashboard/stream", (
    DashboardService dashboard,
    CancellationToken ct) =>
{
    async IAsyncEnumerable<DashboardUpdate> StreamUpdates(
        [EnumeratorCancellation] CancellationToken token)
    {
        while (!token.IsCancellationRequested)
        {
            yield return await dashboard.GetLatestMetricsAsync(token);
            await Task.Delay(TimeSpan.FromSeconds(2), token);
        }
    }

    return Results.ServerSentEvents(StreamUpdates(ct));
});

SignalR hay SSE thuần?

Nếu tính năng real-time của bạn chỉ cần server push một chiều (dashboard metrics, notification feed, progress bar), SSE với Results.ServerSentEvents trên .NET 10 là lựa chọn đơn giản hơn — không cần Hub, không cần client library, không cần sticky sessions hay backplane. SignalR mạnh hơn khi cần giao tiếp hai chiều (chat, collaborative editing, game state sync) hoặc cần quản lý Groups/Users phức tạp.

3. Patterns thực chiến với SignalR

3.1. Chat Room với Groups và Presence

Đây là use case kinh điển nhất của SignalR. Groups cho phép broadcast message tới một tập con clients, trong khi presence tracking theo dõi ai đang online.

public class ChatHub : Hub<IChatClient>
{
    private static readonly ConcurrentDictionary<string, UserInfo> _onlineUsers = new();

    public override async Task OnConnectedAsync()
    {
        var userId = Context.UserIdentifier!;
        var userName = Context.User!.FindFirst("name")!.Value;

        _onlineUsers.TryAdd(Context.ConnectionId, new UserInfo(userId, userName));

        // Broadcast danh sách online users cho client mới
        await Clients.Caller.OnlineUsers(_onlineUsers.Values.ToList());
        await Clients.Others.UserJoined(userName);
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        if (_onlineUsers.TryRemove(Context.ConnectionId, out var user))
            await Clients.Others.UserLeft(user.Name);
    }

    public async Task SendToRoom(string room, string message)
    {
        var sender = _onlineUsers[Context.ConnectionId];
        var chatMessage = new ChatMessage(sender.Name, message, DateTime.UtcNow);

        await Clients.Group(room).ReceiveMessage(chatMessage);
    }

    public async Task JoinRoom(string room)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, room);
        var sender = _onlineUsers[Context.ConnectionId];
        await Clients.Group(room).UserJoined(sender.Name);
    }

    public async Task LeaveRoom(string room)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, room);
        var sender = _onlineUsers[Context.ConnectionId];
        await Clients.Group(room).UserLeft(sender.Name);
    }
}

3.2. Real-Time Dashboard với Background Service

Trong nhiều trường hợp, data real-time không đến từ client mà từ background process — worker xử lý đơn hàng, metrics collector, hoặc event consumer. SignalR cho phép inject IHubContext vào bất kỳ service nào để push data tới clients.

graph LR
    DB["Database
Event Store"] -->|"Change Event"| BG["Background
Service"] KAFKA["Kafka / Redis
Stream"] -->|"Consume"| BG BG -->|"IHubContext"| HUB["SignalR Hub"] HUB -->|"Push Update"| D1["Dashboard 1"] HUB -->|"Push Update"| D2["Dashboard 2"] HUB -->|"Push Update"| D3["Mobile App"] style HUB fill:#e94560,stroke:#fff,color:#fff style BG fill:#2c3e50,stroke:#e94560,color:#fff style DB fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style KAFKA fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style D1 fill:#2c3e50,stroke:#e94560,color:#fff style D2 fill:#2c3e50,stroke:#e94560,color:#fff style D3 fill:#2c3e50,stroke:#e94560,color:#fff

Hình 3: Push data từ background service tới dashboard clients qua IHubContext

// Background Service push metrics tới dashboard mỗi 2 giây
public class MetricsPushService : BackgroundService
{
    private readonly IHubContext<DashboardHub, IDashboardClient> _hubContext;
    private readonly IMetricsCollector _metrics;

    public MetricsPushService(
        IHubContext<DashboardHub, IDashboardClient> hubContext,
        IMetricsCollector metrics)
    {
        _hubContext = hubContext;
        _metrics = metrics;
    }

    protected override async Task ExecuteAsync(CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            var snapshot = await _metrics.CollectAsync(ct);

            // Push tới tất cả clients đang xem dashboard
            await _hubContext.Clients
                .Group("dashboard-viewers")
                .MetricsUpdated(new DashboardSnapshot
                {
                    ActiveUsers = snapshot.ActiveUsers,
                    RequestsPerSecond = snapshot.RPS,
                    ErrorRate = snapshot.ErrorRate,
                    P99Latency = snapshot.P99,
                    Timestamp = DateTime.UtcNow
                });

            await Task.Delay(TimeSpan.FromSeconds(2), ct);
        }
    }
}

3.3. Targeted Notification — Gửi đúng người, đúng lúc

SignalR hỗ trợ gửi message tới một user cụ thể thông qua Clients.User(userId). User identifier được map tự động từ ClaimTypes.NameIdentifier trong JWT token hoặc cookie authentication.

// Gửi notification tới một user cụ thể từ bất kỳ đâu trong app
public class OrderService
{
    private readonly IHubContext<NotificationHub, INotificationClient> _hub;

    public async Task CompleteOrderAsync(Order order)
    {
        await _repository.UpdateStatusAsync(order.Id, OrderStatus.Completed);

        // Push notification real-time tới user (dù họ mở nhiều tab/device)
        await _hub.Clients.User(order.UserId).OrderStatusChanged(new OrderNotification
        {
            OrderId = order.Id,
            Status = "Completed",
            Message = $"Đơn hàng #{order.Id} đã hoàn thành!",
            Timestamp = DateTime.UtcNow
        });
    }
}

User vs ConnectionId

Clients.User(userId) gửi tới tất cả connections của một user (nhiều tab, nhiều device). Clients.Client(connectionId) chỉ gửi tới một connection cụ thể. Trong hầu hết use case notification, hãy dùng User() — user mở app trên cả laptop và điện thoại đều nhận được thông báo.

4. SignalR Client — JavaScript và Vue.js

Phía client, SignalR cung cấp thư viện @microsoft/signalr cho JavaScript/TypeScript. Tích hợp với Vue.js thông qua composable pattern.

// composables/useSignalR.ts — Vue 3 composable cho SignalR
import { ref, onMounted, onUnmounted } from 'vue'
import * as signalR from '@microsoft/signalr'

export function useSignalR(hubUrl: string) {
  const connection = ref<signalR.HubConnection | null>(null)
  const isConnected = ref(false)

  const start = async () => {
    connection.value = new signalR.HubConnectionBuilder()
      .withUrl(hubUrl, {
        accessTokenFactory: () => localStorage.getItem('token') || ''
      })
      .withAutomaticReconnect([0, 2000, 5000, 10000, 30000])
      .configureLogging(signalR.LogLevel.Warning)
      .build()

    connection.value.onreconnecting(() => {
      isConnected.value = false
    })

    connection.value.onreconnected(() => {
      isConnected.value = true
    })

    connection.value.onclose(() => {
      isConnected.value = false
    })

    await connection.value.start()
    isConnected.value = true
  }

  const on = (method: string, callback: (...args: any[]) => void) => {
    connection.value?.on(method, callback)
  }

  const invoke = async (method: string, ...args: any[]) => {
    return connection.value?.invoke(method, ...args)
  }

  onMounted(start)
  onUnmounted(() => connection.value?.stop())

  return { connection, isConnected, on, invoke }
}
<!-- components/ChatRoom.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useSignalR } from '@/composables/useSignalR'

const { on, invoke, isConnected } = useSignalR('/hubs/chat')
const messages = ref<ChatMessage[]>([])
const newMessage = ref('')
const currentRoom = ref('general')

onMounted(() => {
  on('ReceiveMessage', (msg: ChatMessage) => {
    messages.value.push(msg)
  })

  on('UserJoined', (userName: string) => {
    messages.value.push({
      sender: 'System',
      content: `${userName} đã tham gia phòng`,
      timestamp: new Date()
    })
  })

  invoke('JoinRoom', currentRoom.value)
})

const sendMessage = async () => {
  if (!newMessage.value.trim()) return
  await invoke('SendToRoom', currentRoom.value, newMessage.value)
  newMessage.value = ''
}
</script>

5. Scale-Out — Từ single server tới triệu connections

Mặc định, SignalR lưu connection state trong memory của một process. Khi deploy nhiều server instances (horizontal scaling), client A kết nối server 1 không thể nhận message từ client B kết nối server 2. Đây là bài toán scale-out cần giải quyết.

5.1. Redis Backplane — Scale-out tự quản lý

Redis Backplane sử dụng Redis Pub/Sub để đồng bộ messages giữa các server instances. Khi server 1 cần broadcast message, nó publish lên Redis channel; tất cả server khác subscribe channel đó và forward message tới clients của mình.

graph TB
    C1["Client A"] --> S1["Server 1"]
    C2["Client B"] --> S1
    C3["Client C"] --> S2["Server 2"]
    C4["Client D"] --> S2
    C5["Client E"] --> S3["Server 3"]
    S1 <-->|"Pub/Sub"| REDIS["Redis
Backplane"] S2 <-->|"Pub/Sub"| REDIS S3 <-->|"Pub/Sub"| REDIS LB["Load Balancer
(Sticky Sessions)"] --> S1 LB --> S2 LB --> S3 style REDIS fill:#e94560,stroke:#fff,color:#fff style LB fill:#2c3e50,stroke:#e94560,color:#fff style S1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style S2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style S3 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style C1 fill:#2c3e50,stroke:#e94560,color:#fff style C2 fill:#2c3e50,stroke:#e94560,color:#fff style C3 fill:#2c3e50,stroke:#e94560,color:#fff style C4 fill:#2c3e50,stroke:#e94560,color:#fff style C5 fill:#2c3e50,stroke:#e94560,color:#fff

Hình 4: Redis Backplane — đồng bộ messages giữa nhiều server instances qua Pub/Sub

// Program.cs — Cấu hình Redis Backplane
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR()
    .AddStackExchangeRedis(options =>
    {
        options.Configuration = builder.Configuration
            .GetConnectionString("Redis")!;
        options.Configuration.ChannelPrefix =
            RedisChannel.Literal("ChatApp");
    });

var app = builder.Build();
app.MapHub<ChatHub>("/hubs/chat");
app.Run();

Redis Backplane vẫn cần Sticky Sessions!

SignalR sử dụng quy trình negotiate hai bước: bước 1 client gọi /negotiate để lấy connectionId, bước 2 client mở transport (WebSocket/SSE) với connectionId đó. Hai request này phải đến cùng một server — nếu không, server thứ hai sẽ không tìm thấy connection context. Cấu hình load balancer với sticky sessions (dựa trên cookie hoặc IP) là bắt buộc khi dùng Redis Backplane.

5.2. Azure SignalR Service — Managed Scale-out

Nếu bạn deploy trên Azure và muốn tránh quản lý Redis backplane + sticky sessions, Azure SignalR Service là giải pháp managed. Toàn bộ WebSocket connections được quản lý bởi Azure — app server chỉ xử lý business logic, không giữ connection state.

graph LR
    C1["Clients"] -->|"WebSocket"| AZURE["Azure SignalR
Service"] AZURE -->|"Server Connection"| S1["App Server 1"] AZURE -->|"Server Connection"| S2["App Server 2"] S1 --> DB["Database"] S2 --> DB style AZURE fill:#e94560,stroke:#fff,color:#fff style S1 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style S2 fill:#f8f9fa,stroke:#e94560,color:#2c3e50 style C1 fill:#2c3e50,stroke:#e94560,color:#fff style DB fill:#f8f9fa,stroke:#e94560,color:#2c3e50

Hình 5: Azure SignalR Service — clients kết nối tới Azure, app server chỉ xử lý logic

// Program.cs — Azure SignalR Service
builder.Services.AddSignalR()
    .AddAzureSignalR(options =>
    {
        options.ConnectionString = builder.Configuration
            .GetConnectionString("AzureSignalR")!;
        options.ServerStickyMode =
            ServerStickyMode.Required; // Đảm bảo consistency
    });
Tiêu chíRedis BackplaneAzure SignalR Service
Sticky SessionsBắt buộcKhông cần
Connection managementApp server giữAzure quản lý
Scale limitPhụ thuộc Redis + serverHàng triệu connections
LatencyThấp (cùng datacenter)Thêm 1 hop tới Azure
CostChi phí Redis serverPer unit pricing (~$50/unit)
Message loss khi Redis downCó (không buffer)Azure đảm bảo delivery
Chọn khiSelf-hosted, latency-criticalAzure-hosted, cần scale >10K connections

6. SignalR vs SSE vs Raw WebSocket — Chọn đúng công cụ

Không phải mọi tính năng real-time đều cần SignalR. Dưới đây là framework quyết định dựa trên yêu cầu cụ thể:

Yêu cầuGiải phápLý do
Server push một chiều (dashboard, notifications)SSE + Results.ServerSentEventsKhông cần client library, không cần backplane, HTTP/2 multiplexing
Chat, collaborative editing, gameSignalRTwo-way communication, Groups, Users, auto-reconnect, type safety
Custom binary protocol, video/audio streamingRaw WebSocketFull control over framing, no overhead từ SignalR protocol
IoT sensor data (hàng nghìn devices)MQTT hoặc Raw WebSocketSignalR overhead không phù hợp cho IoT scale
Cần scale >100K connections trên AzureAzure SignalR ServiceManaged, không cần sticky sessions, auto-scale

Nguyên tắc chung cho 2026

Phần lớn tính năng "real-time" trong ứng dụng enterprise thực chất chỉ cần unidirectional push — dashboard metrics, order status, notification feed. Trong những trường hợp này, SSE với Results.ServerSentEvents trên .NET 10 là lựa chọn đơn giản nhất với ít infrastructure overhead nhất. SignalR nên được dùng khi bạn thực sự cần bidirectional communication hoặc các tính năng cao cấp như Groups, Presence, và Strongly-Typed Hub.

7. Authentication và Authorization cho SignalR

SignalR Hub có thể sử dụng cùng authentication/authorization pipeline với ASP.NET Core. Với JWT Bearer token, client truyền token qua query string (vì WebSocket không hỗ trợ custom headers trong browser).

// Program.cs — Authentication cho SignalR Hub
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                // SignalR truyền token qua query string
                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;
            }
        };
    });

// Hub với Authorization
[Authorize]
public class SecureChatHub : Hub<IChatClient>
{
    [Authorize(Roles = "Admin")]
    public async Task DeleteMessage(string messageId)
    {
        await Clients.All.MessageDeleted(messageId);
    }

    public async Task SendMessage(string message)
    {
        var userId = Context.UserIdentifier!;
        var userName = Context.User!.FindFirst("name")!.Value;
        await Clients.Others.ReceiveMessage(
            new ChatMessage(userName, message, DateTime.UtcNow));
    }
}

8. Best Practices và Anti-Patterns

8.1. Quản lý Connection Lifetime

// Client-side: Automatic Reconnect với exponential backoff
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/hubs/chat")
    .withAutomaticReconnect({
        nextRetryDelayInMilliseconds: (retryContext) => {
            // Exponential backoff: 0s, 2s, 4s, 8s, 16s, max 30s
            const delay = Math.min(
                Math.pow(2, retryContext.previousRetryCount) * 1000,
                30000
            );
            return delay;
        }
    })
    .build();

// Xử lý reconnection events
connection.onreconnecting((error) => {
    showToast("Đang kết nối lại...", "warning");
});

connection.onreconnected((connectionId) => {
    showToast("Đã kết nối lại!", "success");
    // Re-join rooms sau reconnect
    connection.invoke("JoinRoom", currentRoom);
});

connection.onclose((error) => {
    showToast("Mất kết nối. Vui lòng tải lại trang.", "error");
});

8.2. Anti-Patterns cần tránh

Anti-PatternVấn đềGiải pháp
Gửi large payload qua HubBlock connection, tăng memory pressureUpload file qua HTTP, chỉ gửi notification qua SignalR
Blocking call trong Hub methodBlock I/O thread, giảm throughputLuôn dùng async/await, không Task.Result
Lưu state trong static field của HubHub instance được tạo mới mỗi invocationDùng DI service (Singleton/Scoped) hoặc IHubContext
Không handle reconnectionUser mất kết nối không biếtwithAutomaticReconnect() + UI feedback
Broadcast quá thường xuyênFlood client, tăng bandwidthThrottle/debounce trên server, chỉ gửi delta

Hub instance là Transient!

Mỗi khi client gọi một Hub method, ASP.NET Core tạo một Hub instance mới. Đừng lưu state vào property hoặc field của Hub class — nó sẽ bị mất sau mỗi invocation. Thay vào đó, dùng IHubContext inject vào Singleton service hoặc sử dụng external storage (Redis, database) cho shared state.

8.3. Performance Tuning

// Program.cs — Tối ưu SignalR cho production
builder.Services.AddSignalR(options =>
{
    options.EnableDetailedErrors = false; // Production: tắt detailed errors
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
    options.HandshakeTimeout = TimeSpan.FromSeconds(5);
    options.MaximumReceiveMessageSize = 64 * 1024; // 64KB max message
    options.StreamBufferCapacity = 10;
})
.AddMessagePackProtocol(); // Binary protocol thay vì JSON — nhỏ hơn ~30%

MessagePack Protocol

Mặc định SignalR dùng JSON protocol. Chuyển sang MessagePack (binary serialization) giảm ~30% kích thước message và tăng tốc serialization/deserialization. Đặc biệt hiệu quả khi gửi data có nhiều số (dashboard metrics, game state). Cài đặt: dotnet add package Microsoft.AspNetCore.SignalR.Protocols.MessagePack.

9. Kết luận

SignalR trên .NET 10 tiếp tục là giải pháp real-time production-ready mạnh mẽ nhất trong hệ sinh thái .NET. Với Strongly-Typed Hub, automatic transport negotiation, và khả năng scale-out qua Redis Backplane hoặc Azure SignalR Service, nó giải quyết trọn vẹn bài toán giao tiếp real-time — từ chat đơn giản đến dashboard triệu connections.

Tuy nhiên, .NET 10 cũng mang đến Results.ServerSentEvents — một lựa chọn nhẹ hơn đáng kể cho các tính năng chỉ cần server push một chiều. Nguyên tắc chọn: SSE cho unidirectional push (dashboard, notification feed), SignalR cho bidirectional communication (chat, collaboration), và raw WebSocket cho custom binary protocol. Đừng mặc định dùng SignalR cho mọi tính năng real-time — chọn đúng công cụ giúp giảm complexity và infrastructure cost đáng kể.

Nguồn tham khảo