dotnet-signalr

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SignalR

SignalR

Trigger On

触发场景

  • building chat, notification, collaboration, or live-update features
  • debugging hub lifetime, connection state, or transport issues
  • deciding whether SignalR or another transport better fits the scenario
  • implementing real-time broadcasting to groups of connected clients
  • scaling SignalR across multiple servers
  • 构建聊天、通知、协作或者实时更新功能
  • 调试集线器生命周期、连接状态或者传输协议相关问题
  • 确定SignalR或其他传输协议是否更适配当前场景
  • 实现向已连接客户端群组的实时广播
  • 跨多服务器扩展SignalR服务

Documentation

参考文档

References

相关参考

  • patterns.md - Detailed hub patterns, streaming, groups, presence, and advanced messaging techniques
  • anti-patterns.md - Common SignalR mistakes and how to avoid them
  • patterns.md - 详细的集线器模式、流传输、分组、在线状态和高级消息处理技术
  • anti-patterns.md - 常见的SignalR错误以及规避方法

Workflow

工作流程

  1. Use SignalR for broadcast-style or connection-oriented real-time features; do not force gRPC into hub-style fan-out scenarios.
  2. Model hub contracts intentionally and keep hub methods thin, delegating durable work elsewhere.
  3. Plan for reconnection, backpressure, auth, and fan-out costs instead of treating real-time messaging as stateless request/response.
  4. Use groups, presence, and connection metadata deliberately so scale-out behavior is understandable.
  5. If Native AOT or trimming is in play, validate supported protocols and serialization choices explicitly.
  6. Test connection behavior and failure modes, not just happy-path message delivery.
  1. 面向广播类或面向连接的实时功能使用SignalR;不要强制将gRPC用于集线器类的扇出场景。
  2. 有意识地设计集线器契约,保持集线器方法精简,将持久化工作委托给其他模块处理。
  3. 提前规划重连、背压、认证和扇出成本,不要将实时消息传递视为无状态的请求/响应模式。
  4. 有意识地使用分组、在线状态和连接元数据,确保扩展行为可预期。
  5. 如果使用Native AOT或裁剪功能,需显式验证支持的协议和序列化方案。
  6. 测试连接行为和故障模式,而不仅仅是正常路径下的消息传递。

Hub Patterns

集线器模式

Strongly-Typed Hub (Recommended)

强类型集线器(推荐)

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

// Implement the strongly-typed hub
public class ChatHub : Hub<IChatClient>
{
    public async Task SendMessage(string user, string message)
    {
        // Compiler checks client method calls
        await Clients.All.ReceiveMessage(user, message);
    }

    public override async Task OnConnectedAsync()
    {
        await Clients.Others.UserJoined(Context.User?.Identity?.Name ?? "Anonymous");
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        await Clients.Others.UserLeft(Context.User?.Identity?.Name ?? "Anonymous");
        await base.OnDisconnectedAsync(exception);
    }
}
csharp
// Define the client interface
public interface IChatClient
{
    Task ReceiveMessage(string user, string message);
    Task UserJoined(string user);
    Task UserLeft(string user);
}

// Implement the strongly-typed hub
public class ChatHub : Hub<IChatClient>
{
    public async Task SendMessage(string user, string message)
    {
        // Compiler checks client method calls
        await Clients.All.ReceiveMessage(user, message);
    }

    public override async Task OnConnectedAsync()
    {
        await Clients.Others.UserJoined(Context.User?.Identity?.Name ?? "Anonymous");
        await base.OnConnectedAsync();
    }

    public override async Task OnDisconnectedAsync(Exception? exception)
    {
        await Clients.Others.UserLeft(Context.User?.Identity?.Name ?? "Anonymous");
        await base.OnDisconnectedAsync(exception);
    }
}

Using Groups for Targeted Messaging

使用分组实现定向消息推送

csharp
public class NotificationHub : Hub<INotificationClient>
{
    public async Task JoinGroup(string groupName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        await Clients.Group(groupName).UserJoined(Context.User?.Identity?.Name);
    }

    public async Task LeaveGroup(string groupName)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
    }

    public async Task SendToGroup(string groupName, string message)
    {
        await Clients.Group(groupName).ReceiveNotification(message);
    }
}
csharp
public class NotificationHub : Hub<INotificationClient>
{
    public async Task JoinGroup(string groupName)
    {
        await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
        await Clients.Group(groupName).UserJoined(Context.User?.Identity?.Name);
    }

    public async Task LeaveGroup(string groupName)
    {
        await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
    }

    public async Task SendToGroup(string groupName, string message)
    {
        await Clients.Group(groupName).ReceiveNotification(message);
    }
}

Hub Method with Custom Object Parameters (API Versioning)

带自定义对象参数的集线器方法(API版本控制)

csharp
// Use custom objects to avoid breaking changes
public class SendMessageRequest
{
    public string Message { get; set; } = string.Empty;
    public string? Recipient { get; set; }  // Added later without breaking clients
    public int? Priority { get; set; }       // Added later without breaking clients
}

public class ChatHub : Hub<IChatClient>
{
    public async Task SendMessage(SendMessageRequest request)
    {
        // Handle both old and new clients
        if (request.Recipient != null)
        {
            await Clients.User(request.Recipient).ReceiveMessage(request.Message);
        }
        else
        {
            await Clients.All.ReceiveMessage(request.Message);
        }
    }
}
csharp
// Use custom objects to avoid breaking changes
public class SendMessageRequest
{
    public string Message { get; set; } = string.Empty;
    public string? Recipient { get; set; }  // Added later without breaking clients
    public int? Priority { get; set; }       // Added later without breaking clients
}

public class ChatHub : Hub<IChatClient>
{
    public async Task SendMessage(SendMessageRequest request)
    {
        // Handle both old and new clients
        if (request.Recipient != null)
        {
            await Clients.User(request.Recipient).ReceiveMessage(request.Message);
        }
        else
        {
            await Clients.All.ReceiveMessage(request.Message);
        }
    }
}

Client Patterns

客户端模式

JavaScript Client with Automatic Reconnection

支持自动重连的JavaScript客户端

javascript
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chatHub")
    .withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) // Retry delays
    .configureLogging(signalR.LogLevel.Information)
    .build();

// Handle reconnection events
connection.onreconnecting(error => {
    console.log("Reconnecting...", error);
    updateUIForReconnecting();
});

connection.onreconnected(connectionId => {
    console.log("Reconnected with ID:", connectionId);
    // Rejoin groups - reconnection does not restore group membership
    rejoinGroups();
    updateUIForConnected();
});

connection.onclose(error => {
    console.log("Connection closed", error);
    updateUIForDisconnected();
});

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

start();
javascript
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/chatHub")
    .withAutomaticReconnect([0, 2000, 5000, 10000, 30000]) // Retry delays
    .configureLogging(signalR.LogLevel.Information)
    .build();

// Handle reconnection events
connection.onreconnecting(error => {
    console.log("Reconnecting...", error);
    updateUIForReconnecting();
});

connection.onreconnected(connectionId => {
    console.log("Reconnected with ID:", connectionId);
    // Rejoin groups - reconnection does not restore group membership
    rejoinGroups();
    updateUIForConnected();
});

connection.onclose(error => {
    console.log("Connection closed", error);
    updateUIForDisconnected();
});

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

start();

.NET Client with Reconnection

支持重连的.NET客户端

csharp
var connection = new HubConnectionBuilder()
    .WithUrl("https://localhost:5001/chatHub", options =>
    {
        options.AccessTokenProvider = () => Task.FromResult(GetAccessToken());
    })
    .WithAutomaticReconnect()
    .Build();

connection.Reconnecting += error =>
{
    _logger.LogWarning("Connection lost. Reconnecting: {Error}", error?.Message);
    return Task.CompletedTask;
};

connection.Reconnected += connectionId =>
{
    _logger.LogInformation("Reconnected with ID: {ConnectionId}", connectionId);
    // Rejoin groups after reconnection
    return RejoinGroupsAsync();
};

connection.Closed += async error =>
{
    _logger.LogError("Connection closed: {Error}", error?.Message);
    await Task.Delay(Random.Shared.Next(0, 5) * 1000);
    await connection.StartAsync();
};

await connection.StartAsync();
csharp
var connection = new HubConnectionBuilder()
    .WithUrl("https://localhost:5001/chatHub", options =>
    {
        options.AccessTokenProvider = () => Task.FromResult(GetAccessToken());
    })
    .WithAutomaticReconnect()
    .Build();

connection.Reconnecting += error =>
{
    _logger.LogWarning("Connection lost. Reconnecting: {Error}", error?.Message);
    return Task.CompletedTask;
};

connection.Reconnected += connectionId =>
{
    _logger.LogInformation("Reconnected with ID: {ConnectionId}", connectionId);
    // Rejoin groups after reconnection
    return RejoinGroupsAsync();
};

connection.Closed += async error =>
{
    _logger.LogError("Connection closed: {Error}", error?.Message);
    await Task.Delay(Random.Shared.Next(0, 5) * 1000);
    await connection.StartAsync();
};

await connection.StartAsync();

Server Configuration

服务端配置

Hub Registration with Authentication

带身份认证的集线器注册

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR(options =>
{
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.MaximumReceiveMessageSize = 64 * 1024; // 64 KB
    options.StreamBufferCapacity = 10;
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
})
.AddMessagePackProtocol(); // Binary protocol for performance

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                // Read token from query string for WebSocket connections
                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;
            }
        };
    });

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapHub<ChatHub>("/hubs/chat");
csharp
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR(options =>
{
    options.EnableDetailedErrors = builder.Environment.IsDevelopment();
    options.MaximumReceiveMessageSize = 64 * 1024; // 64 KB
    options.StreamBufferCapacity = 10;
    options.KeepAliveInterval = TimeSpan.FromSeconds(15);
    options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
})
.AddMessagePackProtocol(); // Binary protocol for performance

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                // Read token from query string for WebSocket connections
                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;
            }
        };
    });

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

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

Sending Messages from Outside a Hub

从集线器外部发送消息

csharp
public class NotificationService
{
    private readonly IHubContext<NotificationHub, INotificationClient> _hubContext;

    public NotificationService(IHubContext<NotificationHub, INotificationClient> hubContext)
    {
        _hubContext = hubContext;
    }

    public async Task NotifyAllAsync(string message)
    {
        await _hubContext.Clients.All.ReceiveNotification(message);
    }

    public async Task NotifyUserAsync(string userId, string message)
    {
        await _hubContext.Clients.User(userId).ReceiveNotification(message);
    }

    public async Task NotifyGroupAsync(string groupName, string message)
    {
        await _hubContext.Clients.Group(groupName).ReceiveNotification(message);
    }
}
csharp
public class NotificationService
{
    private readonly IHubContext<NotificationHub, INotificationClient> _hubContext;

    public NotificationService(IHubContext<NotificationHub, INotificationClient> hubContext)
    {
        _hubContext = hubContext;
    }

    public async Task NotifyAllAsync(string message)
    {
        await _hubContext.Clients.All.ReceiveNotification(message);
    }

    public async Task NotifyUserAsync(string userId, string message)
    {
        await _hubContext.Clients.User(userId).ReceiveNotification(message);
    }

    public async Task NotifyGroupAsync(string groupName, string message)
    {
        await _hubContext.Clients.Group(groupName).ReceiveNotification(message);
    }
}

Scaling with Redis Backplane

使用Redis背板实现扩展

csharp
builder.Services.AddSignalR()
    .AddStackExchangeRedis(connectionString, options =>
    {
        options.Configuration.ChannelPrefix = RedisChannel.Literal("MyApp");
    });
csharp
builder.Services.AddSignalR()
    .AddStackExchangeRedis(connectionString, options =>
    {
        options.Configuration.ChannelPrefix = RedisChannel.Literal("MyApp");
    });

Anti-Patterns to Avoid

需要避免的反模式

Anti-PatternWhy It's BadBetter Approach
Storing state in Hub propertiesHub instances are created per method callUse
IMemoryCache
, database, or external store
Instantiating Hub directlyBypasses SignalR infrastructureUse
IHubContext<THub>
for external messaging
Not awaiting
SendAsync
calls
Messages may not be sent before hub method completesAlways
await
async hub calls
Adding method parameters without versioningBreaking change for existing clientsUse custom object parameters
Ignoring reconnection group lossClients lose group membership on reconnectRe-add to groups in
OnConnectedAsync
or client reconnect handler
Large payloads over SignalRMemory pressure, bandwidth issuesUse REST/gRPC for bulk data, SignalR for notifications
Missing backplane in multi-serverMessages only reach clients on same serverUse Redis backplane or Azure SignalR Service
Exposing ORM entities directlyMay serialize sensitive dataUse DTOs with explicit properties
Not validating incoming messagesSecurity risk after initial authValidate every hub method input
反模式问题原因优化方案
在集线器属性中存储状态集线器实例会在每次方法调用时重新创建使用
IMemoryCache
、数据库或外部存储
直接实例化集线器会绕过SignalR基础框架使用
IHubContext<THub>
实现外部消息推送
不等待
SendAsync
调用
消息可能在集线器方法执行完成前未成功发送始终
await
异步集线器调用
未做版本控制就添加方法参数会对现有客户端造成破坏性变更使用自定义对象作为参数
忽略重连后的分组丢失问题客户端重连后会丢失分组成员身份
OnConnectedAsync
或客户端重连处理逻辑中重新添加分组
通过SignalR传输大负载会造成内存压力、带宽占用过高批量数据使用REST/gRPC传输,SignalR仅用于通知
多服务器部署时缺少背板消息仅能送达同一服务器上的客户端使用Redis背板或Azure SignalR Service
直接暴露ORM实体可能序列化敏感数据使用仅包含显式属性的DTO
未校验传入消息初始认证后存在安全风险校验所有集线器方法的入参

Best Practices

最佳实践

Connection Management

连接管理

  1. Enable automatic reconnection with exponential backoff delays
  2. Handle group rejoining explicitly after reconnection (connection ID changes)
  3. Implement heartbeat monitoring on the client to detect stale connections
  4. Use sticky sessions when scaling across multiple servers (unless using Azure SignalR Service)
  1. 开启自动重连并配置指数退避重试延迟
  2. 显式处理重连后的重新入组逻辑(重连后连接ID会变更)
  3. 在客户端实现心跳监测来检测失效连接
  4. 跨多服务器扩展时使用粘性会话(除非使用Azure SignalR Service)

Performance

性能优化

  1. Use MessagePack protocol for smaller message sizes and faster serialization
  2. Throttle high-frequency events like typing indicators or mouse movements
  3. Batch messages when possible instead of many small sends
  4. Set appropriate buffer sizes based on expected message throughput
  1. 使用MessagePack协议获得更小的消息体积和更快的序列化速度
  2. 对高频事件做限流,比如输入状态提示、鼠标移动事件
  3. 尽可能批量发送消息,避免大量小消息传输
  4. 根据预期的消息吞吐量设置合适的缓冲区大小

Security

安全规范

  1. Authenticate at connection time using JWT tokens via query string
  2. Authorize hub methods using
    [Authorize]
    attribute
  3. Validate all incoming messages even after authentication
  4. Use HTTPS for all SignalR connections
  1. 连接建立时完成身份认证,通过查询字符串传递JWT令牌
  2. 使用
    [Authorize]
    特性对集线器方法做授权校验
  3. 即使完成身份认证也要校验所有传入消息
  4. 所有SignalR连接使用HTTPS

API Design

API设计

  1. Use strongly-typed hubs to catch client method name typos at compile time
  2. Use custom object parameters to enable backward-compatible API evolution
  3. Version hub names (e.g.,
    ChatHubV2
    ) for breaking changes
  4. Keep hub methods thin and delegate business logic to services
  1. 使用强类型集线器,在编译阶段就能发现客户端方法名拼写错误
  2. 使用自定义对象作为参数,支持API向后兼容迭代
  3. 对集线器名称做版本控制(比如
    ChatHubV2
    )来处理破坏性变更
  4. 保持集线器方法精简,将业务逻辑委托给服务层处理

Observability

可观测性

  1. Log connection events (connect, disconnect, reconnect)
  2. Track transport type used by each connection
  3. Monitor message delivery latency and failure rates
  4. Integrate with Application Insights or other APM tools
  1. 记录连接事件(连接、断开、重连)
  2. 追踪每个连接使用的传输协议类型
  3. 监控消息传递的延迟和失败率
  4. 对接Application Insights或其他APM工具

Deliver

交付要求

  • clear hub contracts and connection behavior
  • real-time delivery that matches the product scenario
  • validation for reconnection and authorization flows
  • appropriate scale-out strategy for multi-server deployments
  • 清晰的集线器契约和连接行为定义
  • 适配产品场景的实时交付能力
  • 重连和授权流程的校验逻辑
  • 适配多服务器部署的合理扩展策略

Validate

验证项

  • SignalR is the correct transport for the use case
  • hub methods remain orchestration-oriented
  • group and auth behavior are explicit and tested
  • reconnection and group membership are handled correctly
  • backplane is configured for multi-server scenarios
  • message validation is implemented in hub methods
  • SignalR是当前场景适配的传输协议
  • 集线器方法始终保持编排定位
  • 分组和认证行为逻辑明确且经过测试
  • 重连和分组成员身份处理逻辑正确
  • 多服务器场景下已配置背板
  • 集线器方法中已实现消息校验逻辑