skills/csharp-signalr/SKILL.md
SignalR Hub 開發規範:Hub Lifetime、群組管理、認證整合、錯誤處理與 Scale-Out 策略。
npx skillsauth add CloudyWing/ai-dotfiles csharp-signalrInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
當偵測到專案使用 SignalR(含 Microsoft.AspNetCore.SignalR 相依套件)或使用者要求撰寫即時通訊功能時,請自動套用以下規範。
IMemoryCache)。// ❌ 錯誤:在 Hub 中儲存狀態
public class ChatHub : Hub {
private readonly List<string> messages = []; // 每次呼叫都是新實例,此欄位無用
public async Task SendMessage(string message) {
messages.Add(message); // 永遠只有一筆
}
}
// ✅ 正確:使用外部服務管理狀態
public class ChatHub(IChatService chatService) : Hub {
public async Task SendMessage(string message) {
await chatService.SaveMessageAsync(message).ConfigureAwait(false);
await Clients.All.SendAsync("ReceiveMessage", message).ConfigureAwait(false);
}
}
SendAsync 的第一個參數)使用 PascalCase,與前端 on 方法對應。SendMessage、JoinRoom、LeaveRoom)。// ✅ Hub 方法可以有回傳值(用戶端可 await 取得結果)
public async Task<IReadOnlyList<MessageDto>> GetRecentMessages(string roomId) {
return await chatService.GetRecentMessagesAsync(roomId).ConfigureAwait(false);
}
使用介面定義用戶端方法,獲得編譯時期型別檢查。
public interface IChatClient {
Task ReceiveMessage(string user, string message);
Task UserJoined(string user);
Task UserLeft(string user);
}
public class ChatHub(IChatService chatService) : Hub<IChatClient> {
public async Task SendMessage(string message) {
string user = Context.User?.Identity?.Name ?? "匿名";
await chatService.SaveMessageAsync(user, message).ConfigureAwait(false);
await Clients.All.ReceiveMessage(user, message).ConfigureAwait(false);
}
}
public override async Task OnConnectedAsync() {
string connectionId = Context.ConnectionId;
string? userId = Context.UserIdentifier;
logger.LogInformation("用戶 {UserId} 已連線,ConnectionId: {ConnectionId}", userId, connectionId);
await base.OnConnectedAsync().ConfigureAwait(false);
}
public override async Task OnDisconnectedAsync(Exception? exception) {
if (exception is not null) {
logger.LogWarning(exception, "用戶 {ConnectionId} 異常斷線", Context.ConnectionId);
}
await base.OnDisconnectedAsync(exception).ConfigureAwait(false);
}
public async Task JoinRoom(string roomId) {
await Groups.AddToGroupAsync(Context.ConnectionId, roomId).ConfigureAwait(false);
await Clients.Group(roomId).UserJoined(Context.User?.Identity?.Name ?? "匿名")
.ConfigureAwait(false);
}
public async Task LeaveRoom(string roomId) {
await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomId).ConfigureAwait(false);
await Clients.Group(roomId).UserLeft(Context.User?.Identity?.Name ?? "匿名")
.ConfigureAwait(false);
}
room:{roomId}、order:{orderId})。在 Controller、BackgroundService 或其他服務中發送 SignalR 訊息,使用 IHubContext<T> 注入。
// 非強型別 Hub
public class NotificationService(IHubContext<NotificationHub> hubContext) {
public async Task NotifyOrderCompletedAsync(int orderId, CancellationToken cancellationToken) {
await hubContext.Clients.Group($"order:{orderId}")
.SendAsync("OrderCompleted", orderId, cancellationToken)
.ConfigureAwait(false);
}
}
// 強型別 Hub
public class NotificationService(IHubContext<NotificationHub, INotificationClient> hubContext) {
public async Task NotifyOrderCompletedAsync(int orderId, CancellationToken cancellationToken) {
await hubContext.Clients.Group($"order:{orderId}")
.OrderCompleted(orderId)
.ConfigureAwait(false);
}
}
// Hub 層級授權
[Authorize]
public class ChatHub : Hub<IChatClient> {
// 方法層級授權
[Authorize(Roles = "Admin")]
public async Task DeleteMessage(int messageId) {
// ...
}
}
builder.Services.AddAuthentication().AddJwtBearer(options => {
options.Events = new JwtBearerEvents {
OnMessageReceived = context => {
string? accessToken = context.Request.Query["access_token"];
PathString path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs")) {
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
builder.Services.AddSignalR(o => o.EnableDetailedErrors = true);HubException:public async Task SendMessage(string roomId, string message) {
if (string.IsNullOrWhiteSpace(message)) {
throw new HubException("訊息內容不可為空。");
}
bool isMember = await chatService.IsMemberAsync(Context.UserIdentifier!, roomId)
.ConfigureAwait(false);
if (!isMember) {
throw new HubException("您不是此聊天室的成員。");
}
// ...
}
// Program.cs
builder.Services.AddSignalR(options => {
options.MaximumReceiveMessageSize = 64 * 1024; // 64 KB
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
options.ClientTimeoutInterval = TimeSpan.FromSeconds(30);
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
});
app.MapHub<ChatHub>("/hubs/chat");
單一伺服器實例時,SignalR 使用 In-Memory 管理連線。多實例部署時,必須使用 Backplane 同步訊息。
builder.Services.AddSignalR()
.AddStackExchangeRedis(connectionString, options => {
options.Configuration.ChannelPrefix = RedisChannel.Literal("MyApp");
});
| Backplane | 適用情境 | | --- | --- | | Redis | 通用首選,支援 Pub/Sub | | Azure SignalR Service | Azure 部署,免管理 Backplane | | SQL Server | 已有 SQL Server 且流量不高 |
tools
PowerShell 腳本撰寫規範:嚴格模式、錯誤處理、參數宣告、Verb-Noun 命名與 5.1 相容語法邊界。當撰寫或修改 `*.ps1` / `*.psm1` 腳本時自動套用。
tools
產生或補齊 .gitattributes,統一行尾處理、二進位識別與 lock files 標記,保留既有自訂偏好。
development
產生或補齊前端 Lint 設定(Prettier + ESLint Flat Config),統一格式化與程式碼品質規則,保留既有自訂偏好。
testing
依據事實校閱報告修改技術文件:以事實層為不可違反的約束,由改檔者負責表達層的措辭與行文連貫。Use when the user asks to apply fact-check results to a document, or to edit a document based on a previously produced fact-check-report.md.