dotnet-signalr
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSignalR
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
工作流程
- Use SignalR for broadcast-style or connection-oriented real-time features; do not force gRPC into hub-style fan-out scenarios.
- Model hub contracts intentionally and keep hub methods thin, delegating durable work elsewhere.
- Plan for reconnection, backpressure, auth, and fan-out costs instead of treating real-time messaging as stateless request/response.
- Use groups, presence, and connection metadata deliberately so scale-out behavior is understandable.
- If Native AOT or trimming is in play, validate supported protocols and serialization choices explicitly.
- Test connection behavior and failure modes, not just happy-path message delivery.
- 面向广播类或面向连接的实时功能使用SignalR;不要强制将gRPC用于集线器类的扇出场景。
- 有意识地设计集线器契约,保持集线器方法精简,将持久化工作委托给其他模块处理。
- 提前规划重连、背压、认证和扇出成本,不要将实时消息传递视为无状态的请求/响应模式。
- 有意识地使用分组、在线状态和连接元数据,确保扩展行为可预期。
- 如果使用Native AOT或裁剪功能,需显式验证支持的协议和序列化方案。
- 测试连接行为和故障模式,而不仅仅是正常路径下的消息传递。
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-Pattern | Why It's Bad | Better Approach |
|---|---|---|
| Storing state in Hub properties | Hub instances are created per method call | Use |
| Instantiating Hub directly | Bypasses SignalR infrastructure | Use |
Not awaiting | Messages may not be sent before hub method completes | Always |
| Adding method parameters without versioning | Breaking change for existing clients | Use custom object parameters |
| Ignoring reconnection group loss | Clients lose group membership on reconnect | Re-add to groups in |
| Large payloads over SignalR | Memory pressure, bandwidth issues | Use REST/gRPC for bulk data, SignalR for notifications |
| Missing backplane in multi-server | Messages only reach clients on same server | Use Redis backplane or Azure SignalR Service |
| Exposing ORM entities directly | May serialize sensitive data | Use DTOs with explicit properties |
| Not validating incoming messages | Security risk after initial auth | Validate every hub method input |
| 反模式 | 问题原因 | 优化方案 |
|---|---|---|
| 在集线器属性中存储状态 | 集线器实例会在每次方法调用时重新创建 | 使用 |
| 直接实例化集线器 | 会绕过SignalR基础框架 | 使用 |
不等待 | 消息可能在集线器方法执行完成前未成功发送 | 始终 |
| 未做版本控制就添加方法参数 | 会对现有客户端造成破坏性变更 | 使用自定义对象作为参数 |
| 忽略重连后的分组丢失问题 | 客户端重连后会丢失分组成员身份 | 在 |
| 通过SignalR传输大负载 | 会造成内存压力、带宽占用过高 | 批量数据使用REST/gRPC传输,SignalR仅用于通知 |
| 多服务器部署时缺少背板 | 消息仅能送达同一服务器上的客户端 | 使用Redis背板或Azure SignalR Service |
| 直接暴露ORM实体 | 可能序列化敏感数据 | 使用仅包含显式属性的DTO |
| 未校验传入消息 | 初始认证后存在安全风险 | 校验所有集线器方法的入参 |
Best Practices
最佳实践
Connection Management
连接管理
- Enable automatic reconnection with exponential backoff delays
- Handle group rejoining explicitly after reconnection (connection ID changes)
- Implement heartbeat monitoring on the client to detect stale connections
- Use sticky sessions when scaling across multiple servers (unless using Azure SignalR Service)
- 开启自动重连并配置指数退避重试延迟
- 显式处理重连后的重新入组逻辑(重连后连接ID会变更)
- 在客户端实现心跳监测来检测失效连接
- 跨多服务器扩展时使用粘性会话(除非使用Azure SignalR Service)
Performance
性能优化
- Use MessagePack protocol for smaller message sizes and faster serialization
- Throttle high-frequency events like typing indicators or mouse movements
- Batch messages when possible instead of many small sends
- Set appropriate buffer sizes based on expected message throughput
- 使用MessagePack协议获得更小的消息体积和更快的序列化速度
- 对高频事件做限流,比如输入状态提示、鼠标移动事件
- 尽可能批量发送消息,避免大量小消息传输
- 根据预期的消息吞吐量设置合适的缓冲区大小
Security
安全规范
- Authenticate at connection time using JWT tokens via query string
- Authorize hub methods using attribute
[Authorize] - Validate all incoming messages even after authentication
- Use HTTPS for all SignalR connections
- 连接建立时完成身份认证,通过查询字符串传递JWT令牌
- 使用特性对集线器方法做授权校验
[Authorize] - 即使完成身份认证也要校验所有传入消息
- 所有SignalR连接使用HTTPS
API Design
API设计
- Use strongly-typed hubs to catch client method name typos at compile time
- Use custom object parameters to enable backward-compatible API evolution
- Version hub names (e.g., ) for breaking changes
ChatHubV2 - Keep hub methods thin and delegate business logic to services
- 使用强类型集线器,在编译阶段就能发现客户端方法名拼写错误
- 使用自定义对象作为参数,支持API向后兼容迭代
- 对集线器名称做版本控制(比如)来处理破坏性变更
ChatHubV2 - 保持集线器方法精简,将业务逻辑委托给服务层处理
Observability
可观测性
- Log connection events (connect, disconnect, reconnect)
- Track transport type used by each connection
- Monitor message delivery latency and failure rates
- Integrate with Application Insights or other APM tools
- 记录连接事件(连接、断开、重连)
- 追踪每个连接使用的传输协议类型
- 监控消息传递的延迟和失败率
- 对接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是当前场景适配的传输协议
- 集线器方法始终保持编排定位
- 分组和认证行为逻辑明确且经过测试
- 重连和分组成员身份处理逻辑正确
- 多服务器场景下已配置背板
- 集线器方法中已实现消息校验逻辑