ねののお庭。

かりかりもふもふ。

【C#】SignalR にも SwaggerUI 的なのがほしい!

この記事は C# Advent Calendar 2022 21 日目の記事です。

背景

みなさん、SignalR は好きですか?私は大好きです。

SignalR は ASP.NET Core にビルトインされているリアルタイム通信機能を提供してくれる大変便利な RPC フレームワーク/ライブラリで、リアルタイム通信機能をアプリに搭載する際にかなりの割合で必要になってくる機能であるところの接続のグルーピングやブロードキャスト、スケールアウト等を容易に行う事ができます。 さらに、複数の接続方式(WebSocket, ServerSentEvents, http long polling)を利用する事ができ、リアルタイム通信を行う際に頻発する接続うまくいかない問題を回避することが出来ます(これはエンプラ環境で大変嬉しい)。 また内部のシリアライズ方式とかも弄れて、C# の高速なシリアライザであるところの MessagePack を利用する事ができるなど、とにかく素敵です。

さて、そんな素敵な SignalR ですが、微妙なポイントが無いわけではありません。 自分が素の SignalR を叩いていて感じる微妙ポイントは2点あります。

まず 1 つめが SignalR のクライアントが強く型付けされないという点です。 サーバサイド、要するに ASP.NET Core 上での SignalR の書き心地は最高です。hub (server side) と receiver (client side) の2つの interface さえ定義してあげれば、強く型付けされた状態で実装が可能です。 一方で、client side の書き心地は server side に比べるとかなり微妙で、hub のメソッド叩くのにもメソッド名を文字列で指定、hub から叩かれるメソッドを登録するのにもメソッド名を文字列で指定して登録...といった感じで文字列を頑張って取り回さないといけなかったり、返り値や引数の型を自分でせっせとつけて回らないといけませんでした。これらの問題を解決するべく、私はこれまでに C# の SignalR client を強く型付けするための TypedSignalR.Client という Incremental Source Generator を活用したライブラリだったり、TypeScript の SignalR client を強く型付け可能するための TypedSignalR.Client.TypeScript という C# コードを解析して TypeScript コードを1コマンドで生成する .NET tool 等のライブラリを開発してきました。実際これで SignalR client を書くのが劇的に楽になり、バグがなくなり、変更も容易になりました。という事でこの問題は解決済み...!

github.com

github.com

そして 2 点目が、REST API (WEB API) における SwaggerUI 的なのが無い事です。 ASP.NET Core ではデフォルトのテンプレートですでに OpenAPI / SwaggerUI が使える状態になっており、これが大変優れもので REST API の開発に大いに役立つことは C# でサーバサイドを書いている人たちには言うまでもないでしょう。 しかし、SignalR には SwaggerUI と同等のものがありません。なので開発する際どうするかというと、実際に SignalR client を組み込む開発中のクライアントサイドのアプリケーション(ネイティブアプリか web のフロントエンド等)から開発中のサーバに接続してみたり、コンソールアプリケーションからサーバに接続してデバッグしていたりしている場合が多いのではないでしょうか。しかし面倒ですよね、そういう開発するの。SwaggerUI 的なのがあれば全てこのあたりのメンドクサさから我々は解放されるハズですし、開発も捗るでしょう。

というわけで、SignalR における SwaggerUI 的役割を果たすものを作りました。その名も TypedSignalR.Client.DevTools です...! README に大量に gif 動画載せてるので README 眺めるだけでなんとなく動きは想像していただけるかと思います。

github.com

TypedSignalR.Client.DevTools の使い方。

とりあえずパッケージを追加しましょう。

dotnet add package TypedSignalR.Client.DevTools

使い方は簡単!必要な工程はたった2つだけです。

1つ目は、SwaggerUI を使う場合と同様に、ミドルウェアを 2 つ追加する事です。SwaggerUI とかと同じで、開発中のみ有効化する事をオススメします。

using TypedSignalR.Client.DevTools; // <- 追加!

var builder = WebApplication.CreateBuilder(args);

// Impl...

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();

    app.UseSignalRHubSpecification(); // <- 追加!
    app.UseSignalRHubDevelopmentUI(); // <- 追加!
}

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

app.MapControllers();

app.Run();

そして 2 つめは、hub と receiver の interface に Attribute をアノテーションしてあげる事です。

using TypedSignalR.Client; // <- 追加!

[Hub] // <- 追加!
public interface IHub
{
    // ...
}

[Receiver] // <- 追加!
public interface IReceiver
{
    // ...
}

// いつも通り Hub を実装
public class MyHub : Hub<IReceiver>, IHub
{
    // ...
}

あとは通常のSignalR の使い方と同じで、MapHub で Hub を http pipeline に追加しましょう。 一点注意点として、MapHub でパスを指定する際には string literal で記述する必要があります。 これはコンパイル時に接続先を確定させる必要があるためです。 なんでそんな仕様にしているかというと、こうすることで、開発用の UI 上では Hub へのパスを入力する必要がなくなり、ボタン一つで Hub に接続できるようになるからです。 Hub のパスを実行時に決定させる事ってほぼほぼ無いと思われるのでこういう風な割り切りをしました。

// Program.cs

// ~いろいろ省略~

app.MapControllers();

app.MapHub<MyHub>("/hubs/MyHub") // 通常通り追加。

app.Run();

これだけです!あとはサーバを立ち上げて、/signalr-dev というエンドポイントをブラウザから叩きましょう!以下のように、開発用の UI が立ち上がり、あとはブラウザからポチポチするだけで SignalR の Hub を叩く事が出来ます。 これで SignalR を使った開発が相当やりやすくなるでしょう...!

https://user-images.githubusercontent.com/27144255/206090956-1d43856b-9088-4af0-a79b-e6ec465156fa.gif

細かい話は GitHub 上に置いてある README を読めば分かりますが、使う上で以下の点さえ押さえてればある程度使えると思います。

  • 組み込み型は普通に文字列を入力すればOK
  • ユーザ定義型は JSON を入力すればOK
  • JWT 認証が利用可能
    • Hub か Hub のメソッドに [Authorize] がアノテーションされていれば、JWT を入力するフィールドが自動で現れます。
  • hub から broadcast されたメッセージ、あるいは server-to-client streaming のメッセージは右のパネルに流れる
  • 返り値のある hub のメソッドの結果は invoke ボタン直下に表示される

TypedSignalR.Client.DevTools そのものの作り。

TypedSignalR.Client.DevTools がどういう作りをしているかというと、ほとんど Swagger のそれと同じ作りをしています。 ASP.NET Core で SwaggerUI を叩けるようになるまでに何が行われているかというと、概ね以下みたいな感じです。

  1. 実行時に Controller とか MapGet とかが使われている箇所を読み取って、swagger.json (REST API の仕様書みたいなもの) を生成する
  2. swagger.json をフロントエンドが読み取って UI を構築

TypedSignalR.Client.DevTools では以下みたいな感じ。だいたい同じ。

  1. コンパイル時に MapHub が呼ばれている箇所を読み取って SignalR Hub の仕様が記述された /signalr-dev/spec.json の中身の文字列を生成
  2. /signalr-dev/spec.json をフロントエンドが読み取って UI を構築

1 で /signalr-dev/spec.json を生成するのは Incremental Source Generator を使っていて、要するにコンパイル時に C# コードをガシガシ解析して、どんな Hub があって、その Hub にはどんなメソッドがあり、返り値やパラメータはうんたらかんたら...みたいな、アプリケーションそれぞれの Hub の仕様が記述された JSON を生成しています。 ちなみに、Source Generator は .NET Standard 2.0 で実装しないといけない縛りが課せられているのですが (Visual Studio が .NET Framework からおさらばしないとこの縛りは続く)、なんと .NET Standard 2.0 では System.Text.Json が使えません...! そしてさらに、なんと Source Generator (というか Analyzer)では外部ライブラリがまともに使えません...! そんな感じでいろいろ縛りがあるため汎用シリアライザが使えないので、涙ぐましく自分で JSON 文字列をくみ上げています。Visual Studio の .NET 化が待ち遠しいですね(遠い目)。また、文字列をそのまま埋め込んだりはせず、UTF8 のバイト列 (byte[]) にして埋め込んでいます。そうすることでエスケープとかなんも考えなくて良くなりますし、実行時軽量になりますからね(まぁ、開発時用のライブラリなので後者のありがたみはそれほどありませんが)。 2 で使ってるフロントエンドについては Next.js で作っていて、ビルドして得られる HTML 及び JavaScript をアセンブリに埋め込んでいます。 /signalr-dev が叩かれるとこの HTML 達が読みだされ、この子たちがコンパイル時に生成された JSON を読み取ってUIを構築する、という感じです。

まとめ

今回 TypedSignalR.Client.DevTools を紹介しました。 これで SignalR でも SwaggerUI 的なものが使えるようになりました...!

というわけで、TypedSignalR.ClientTypedSignalR.Client.TypeScript で SignalR client に強く型付けを行い、TypedSignalR.Client.DevTools で SignalR でも SwaggerUI 的なものが使えるようになった事で、個人的にはもう不満の無い、SignalR を使ったアプリケーションの最高の開発環境が手に入りました。是非 SignalR ユーザの皆さんは TypedSignalR.Client シリーズを使ってみてください、幸せになれるハズです:)