mcpField
S
signalr

signalr

github.com/managedcode開発49661 回インストール今日更新
リポジトリ

signalr について

ASP.NET Coreでリアルタイム機能を構築・運用する開発者向けのSignalRスキルです。Hub設計、接続動作、セキュリティ、テスト、スケーリングの判断を扱います。

主な機能

  1. 01HubコントラクトとAPI設計
  2. 02再接続とグループの処理
  3. 03認証と入力検証
  4. 04トランスポートと障害テスト
  5. 05複数サーバーへの拡張指針

活用シーン

  1. 01チャットや通知機能の構築
  2. 02接続やトランスポート問題の調査
  3. 03クライアントグループへの配信
  4. 04SignalRの複数サーバー構成の計画

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/managedcode/dotnet-skills --skill signalr

インストール後、Claude Code で/signalrを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

manifest.json
references/
anti-patterns.md
patterns.md

詳細

SignalR

トリガー

  • チャット、通知、コラボレーション、またはライブ更新機能を構築するとき
  • ハブのライフタイム、接続状態、またはトランスポートの問題をデバッグするとき
  • SignalR と他のトランスポートのどちらがシナリオに適しているか判断するとき
  • 接続中のクライアントのグループにリアルタイムでブロードキャストする機能を実装するとき
  • 複数のサーバー間で SignalR をスケーリングするとき

ドキュメント

参考資料
  • patterns.md - ハブのパターン、ストリーミング、グループ、プレゼンス、高度なメッセージング手法の詳細
  • anti-patterns.md - よくある SignalR の誤りとその回避方法

ワークフロー

  1. ブロードキャスト型または接続指向のリアルタイム機能には SignalR を使用します。ハブ形式のファンアウトシナリオに gRPC を無理に適用しないでください。
  2. ハブのコントラクトを意図的に設計し、ハブメソッドは薄く保って、永続的な処理は別の場所に委譲します。
  3. リアルタイムメッセージングをステートレスなリクエスト/レスポンスとして扱わず、再接続、バックプレッシャー、認証、ファンアウトのコストを考慮します。
  4. グループ、プレゼンス、接続メタデータを意図的に使用し、スケールアウト時の挙動を把握しやすくします。
  5. Native AOT またはトリミングを使用する場合は、サポート対象のプロトコルとシリアル化の選択肢を明示的に検証します。
  6. 正常な場合のメッセージ配信だけでなく、接続の挙動や障害モードもテストします。

上流の最新情報

  • 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 メソッドの入力をすべて検証します

ベストプラクティス

接続管理
  1. 指数バックオフの遅延を設定して、自動再接続を有効にします
  2. 再接続後(接続 ID が変わります)にグループへ再参加する処理を明示的に行います
  3. 古くなった接続を検出できるよう、クライアントでハートビート監視を実装します
  4. 複数サーバーにスケールする場合は、Azure SignalR Service を使用している場合を除き、スティッキーセッションを使用します
パフォーマンス
  1. メッセージサイズの縮小とシリアル化の高速化のため、MessagePack プロトコルを使用します
  2. 入力中の表示やマウスの動きなど、頻度の高いイベントを抑制します
  3. 可能な場合は、多数の小さなメッセージを送信する代わりに、メッセージをまとめて送信します
  4. 想定されるメッセージ処理量に基づいて、適切なバッファーサイズを設定します
セキュリティ
  1. クエリ文字列を介した JWT トークンを使用して、接続時に認証します
  2. [Authorize] 属性を使用して、Hub メソッドを認可します
  3. 認証後も、受信メッセージをすべて検証します
  4. すべての SignalR 接続で HTTPS を使用します
API 設計
  1. コンパイル時にクライアントメソッド名の誤記を検出できるよう、強く型付けされた Hub を使用します
  2. API を後方互換性を保ちながら進化させるため、カスタムオブジェクトのパラメーターを使用します
  3. 互換性を損なう変更に備えて、Hub 名にバージョンを付けます(例:ChatHubV2)
  4. Hub メソッドは簡潔に保ち、ビジネスロジックはサービスに委譲します
可観測性
  1. 接続イベント(接続、切断、再接続)をログに記録します
  2. 各接続で使用されるトランスポートの種類を追跡します
  3. メッセージ配信のレイテンシと失敗率を監視します
  4. Application Insights やその他の APM ツールと統合します

提供するもの

  • 明確な Hub コントラクトと接続動作
  • プロダクトのシナリオに適したリアルタイム配信
  • 再接続と認可フローの検証
  • 複数サーバー構成に適したスケールアウト戦略

検証項目

  • ユースケースに SignalR が適切なトランスポートであること
  • Hub メソッドがオーケストレーション中心であること
  • グループと認証の動作が明示され、テストされていること
  • 再接続とグループへの所属が正しく処理されていること
  • 複数サーバーのシナリオでバックプレーンが構成されていること
  • Hub メソッドでメッセージの検証が実装されていること

コメント