signalr について
ASP.NET Coreでリアルタイム機能を構築・運用する開発者向けのSignalRスキルです。Hub設計、接続動作、セキュリティ、テスト、スケーリングの判断を扱います。
主な機能
- 01HubコントラクトとAPI設計
- 02再接続とグループの処理
- 03認証と入力検証
- 04トランスポートと障害テスト
- 05複数サーバーへの拡張指針
活用シーン
- 01チャットや通知機能の構築
- 02接続やトランスポート問題の調査
- 03クライアントグループへの配信
- 04SignalRの複数サーバー構成の計画
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ managedcode/ dotnet-skills --skill signalrインストール後、Claude Code で/signalrを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
manifest.jsonreferences/anti-patterns.mdpatterns.md詳細
SignalR
トリガー
- チャット、通知、コラボレーション、またはライブ更新機能を構築するとき
- ハブのライフタイム、接続状態、またはトランスポートの問題をデバッグするとき
- SignalR と他のトランスポートのどちらがシナリオに適しているか判断するとき
- 接続中のクライアントのグループにリアルタイムでブロードキャストする機能を実装するとき
- 複数のサーバー間で SignalR をスケーリングするとき
ドキュメント
参考資料
- patterns.md - ハブのパターン、ストリーミング、グループ、プレゼンス、高度なメッセージング手法の詳細
- anti-patterns.md - よくある SignalR の誤りとその回避方法
ワークフロー
- ブロードキャスト型または接続指向のリアルタイム機能には SignalR を使用します。ハブ形式のファンアウトシナリオに gRPC を無理に適用しないでください。
- ハブのコントラクトを意図的に設計し、ハブメソッドは薄く保って、永続的な処理は別の場所に委譲します。
- リアルタイムメッセージングをステートレスなリクエスト/レスポンスとして扱わず、再接続、バックプレッシャー、認証、ファンアウトのコストを考慮します。
- グループ、プレゼンス、接続メタデータを意図的に使用し、スケールアウト時の挙動を把握しやすくします。
- Native AOT またはトリミングを使用する場合は、サポート対象のプロトコルとシリアル化の選択肢を明示的に検証します。
- 正常な場合のメッセージ配信だけでなく、接続の挙動や障害モードもテストします。
上流の最新情報
dotnet/aspnetcoreのv10.0.11はサービスリリースであり、SignalR のプログラミングモデルに変更はありません。ハブコントラクトの設計、再接続、トランスポート、認可、スケールアウトの検証に重点を置いたガイダンスを維持してください。- 2026 年 8 月時点の ASP.NET Core の概要でも、SignalR はサーバー/クライアント間のリアルタイム通信に適したものとされています。サービス更新後は、依存関係の更新によってクライアントとサーバーのパッケージ不整合が明らかになる場合があるため、再接続とグループへのブロードキャストのスモークテストを少なくとも 1 つ再実行してください。
ハブのパターン
厳密に型指定されたハブ(推奨)
// 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);
}
}
グループを使った対象指定メッセージング
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);
}
}
カスタムオブジェクトのパラメーターを使用するハブメソッド(API バージョニング)
// 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);
}
}
}
クライアントのパターン
自動再接続を使用する 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 クライアント
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();
サーバーの構成
認証を使用するハブの登録
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");
ハブの外部からメッセージを送信する
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);
}
}
Redis バックプレーンによるスケーリング
builder.Services.AddSignalR()
.AddStackExchangeRedis(connectionString, options =>
{
options.Configuration.ChannelPrefix = RedisChannel.Literal("MyApp");
});
避けるべきアンチパターン
| アンチパターン | 問題点 | より良い方法 |
|---|---|---|
| Hub のプロパティに状態を保存する | Hub インスタンスはメソッド呼び出しごとに作成されます | IMemoryCache、データベース、または外部ストアを使用します |
| Hub を直接インスタンス化する | SignalR のインフラストラクチャを経由しません | 外部へのメッセージ送信には IHubContext<THub> を使用します |
SendAsync の呼び出しを await しない | Hub メソッドの完了前にメッセージが送信されない場合があります | 非同期の Hub 呼び出しは必ず await します |
| バージョン管理なしでメソッドのパラメーターを追加する | 既存のクライアントとの互換性を損なう変更になります | カスタムオブジェクトのパラメーターを使用します |
| 再接続時にグループから外れることを考慮しない | 再接続時にクライアントはグループへの所属を失います | OnConnectedAsync またはクライアントの再接続ハンドラーでグループに再追加します |
| SignalR で大きなペイロードを送信する | メモリ負荷や帯域幅の問題が発生します | 大量のデータには REST/gRPC を、通知には SignalR を使用します |
| 複数サーバー構成でバックプレーンがない | メッセージが同じサーバー上のクライアントにしか届きません | Redis バックプレーンまたは Azure SignalR Service を使用します |
| ORM エンティティを直接公開する | 機密データがシリアル化される可能性があります | プロパティを明示した DTO を使用します |
| 受信メッセージを検証しない | 初回認証後もセキュリティリスクがあります | Hub メソッドの入力をすべて検証します |
ベストプラクティス
接続管理
- 指数バックオフの遅延を設定して、自動再接続を有効にします
- 再接続後(接続 ID が変わります)にグループへ再参加する処理を明示的に行います
- 古くなった接続を検出できるよう、クライアントでハートビート監視を実装します
- 複数サーバーにスケールする場合は、Azure SignalR Service を使用している場合を除き、スティッキーセッションを使用します
パフォーマンス
- メッセージサイズの縮小とシリアル化の高速化のため、MessagePack プロトコルを使用します
- 入力中の表示やマウスの動きなど、頻度の高いイベントを抑制します
- 可能な場合は、多数の小さなメッセージを送信する代わりに、メッセージをまとめて送信します
- 想定されるメッセージ処理量に基づいて、適切なバッファーサイズを設定します
セキュリティ
- クエリ文字列を介した JWT トークンを使用して、接続時に認証します
[Authorize]属性を使用して、Hub メソッドを認可します- 認証後も、受信メッセージをすべて検証します
- すべての SignalR 接続で HTTPS を使用します
API 設計
- コンパイル時にクライアントメソッド名の誤記を検出できるよう、強く型付けされた Hub を使用します
- API を後方互換性を保ちながら進化させるため、カスタムオブジェクトのパラメーターを使用します
- 互換性を損なう変更に備えて、Hub 名にバージョンを付けます(例:
ChatHubV2) - Hub メソッドは簡潔に保ち、ビジネスロジックはサービスに委譲します
可観測性
- 接続イベント(接続、切断、再接続)をログに記録します
- 各接続で使用されるトランスポートの種類を追跡します
- メッセージ配信のレイテンシと失敗率を監視します
- Application Insights やその他の APM ツールと統合します
提供するもの
- 明確な Hub コントラクトと接続動作
- プロダクトのシナリオに適したリアルタイム配信
- 再接続と認可フローの検証
- 複数サーバー構成に適したスケールアウト戦略
検証項目
- ユースケースに SignalR が適切なトランスポートであること
- Hub メソッドがオーケストレーション中心であること
- グループと認証の動作が明示され、テストされていること
- 再接続とグループへの所属が正しく処理されていること
- 複数サーバーのシナリオでバックプレーンが構成されていること
- Hub メソッドでメッセージの検証が実装されていること
コメント
Markdown 対応