次の方法で共有


ブローカー サービスを設計するためのベスト プラクティス

StreamJsonRpc の RPC インターフェイス に関する一般的な ガイダンスと制限事項に従ってください。

さらに、ブローカー サービスには次のガイドラインが適用されます。

メソッド シグネチャ

すべてのメソッドは、最後のパラメーターとして CancellationToken パラメーターを受け取る必要があります。 通常、このパラメーターは 省略可能なパラメーター であるべきではありません。そのため、呼び出し元は誤って引数を省略する可能性が低くなります。 メソッドの実装が簡単であると予想される場合でも、CancellationToken を指定すると、クライアントはサーバーに送信される前に独自の要求を取り消すことができます。 また、サーバーの実装は、後でオプションとしてキャンセルを追加するためにメソッドを更新することなく、より高価なものに進化することができます。

RPC インターフェイスで同じメソッドの複数のオーバーロードを "回避" することを検討してください。 オーバーロード解決は通常うまく機能します(これを検証するためにテストを記述する必要があります)が、各オーバーロードのパラメータ型に基づいて引数を逆シリアル化しようとする に依存しているため、オーバーロードを選択する過程で必然的に最初の例外がスローされることがあります。 成功パスでスローされる最初の例外の数を最小限に抑えたいので、特定の名前のメソッドを 1 つだけ使用することをお勧めします。

パラメーターと戻り値の型

RPC で交換されるすべての引数と戻り値は、データ だけであることを覚えておいてください。 これらはすべてシリアル化され、ネットワーク経由で送信されます。 これらのデータ型に対して定義するメソッドは、そのデータのローカル コピーでのみ動作し、データを生成した RPC サービスと通信する方法はありません。 このシリアル化動作の唯一の例外は、StreamJsonRpc が特別なサポートを持つ エキゾチックな型です。

ValueTask<T> では割り当てが少なくなるため、メソッドの戻り値の型として Task<T> に対して ValueTask<T> を使用することを検討してください。 非ジェネリックな品種 (TaskValueTaskなど) を使用する場合は重要度が低くなりますが、ValueTask が望ましい場合があります。 その API に記載されている ValueTask<T> の使用制限に注意してください。 この ブログ投稿のビデオ は、使用する種類を決定するのに役立ちます。

カスタム データ型

すべてのデータ型を不変に定義することを検討してください。これにより、コピーせずにプロセス全体でデータをより安全に共有でき、別の RPC を配置せずにクエリに応答して受信したデータを変更できないという考えをコンシューマーに強化できます。

ServiceJsonRpcDescriptor.Formatters.UTF8を使用する際は、struct ではなく class としてデータ型を定義してください。これは、Newtonsoft.Json を使用する際に生じる (繰り返される可能性のある) ボックス化のコストを回避するためです。 ServiceJsonRpcDescriptor.Formatters.MessagePack の使用時にはボックス化は発生 "しない" ため、そのフォーマッタにコミットしている場合は、構造体が適切なオプションになります。

IEquatable<T> を実装し、データ型に GetHashCode() メソッドと Equals(Object) メソッドをオーバーライドすることを検討してください。これにより、クライアントは、別の時点で受信したデータと等しいかどうかに基づいて、受信したデータを効率的に格納、比較、再利用できます。

JSON を使用したポリモーフィック型のシリアル化をサポートするには、DiscriminatedTypeJsonConverter<TBase> を使用します。

コレクション

具体的な型 (List<T>T[]など) ではなく、RPC メソッド シグネチャ (IReadOnlyList<T>など) で読み取り専用コレクション インターフェイスを使用します。これにより、逆シリアル化の効率が向上する可能性があります。

IEnumerable<T>は避けてください。 Count プロパティがないため、コードが非効率的になり、RPC シナリオでは適用されないデータの遅延生成が発生する可能性があります。 順序付けされていないコレクションには IReadOnlyCollection<T> を使用し、順序付けられたコレクションには IReadOnlyList<T> を使用します。

IAsyncEnumerable<T>を検討してください。 その他のコレクション型または IEnumerable<T> では、コレクション全体が 1 つのメッセージで送信されます。 IAsyncEnumerable<T> を使用すると、小さな初期メッセージが可能になり、受信者はコレクションから必要な数の項目を取得し、非同期的に列挙する手段を提供します。 この新しいパターンについて詳しく知る.

オブザーバー パターン

インターフェイス オブザーバーデザインパターン を使用することを検討してください。 これは、次のセクションで説明する従来のイベント モデルに適用される多くの落とし穴なしに、クライアントがデータをサブスクライブするための簡単な方法です。

オブザーバー パターンは、次のような単純な場合があります。

Task<IDisposable> SubscribeAsync(IObserver<YourDataType> observer);

上記で使用した IDisposable 型と IObserver<T> 型は、StreamJsonRpc で エキゾチックな型の 2 つであるため、単なるデータとしてシリアル化されるのではなく、特別にマーシャリングされた動作を取得します。

イベント

イベントはいくつかの理由で RPC で問題になる可能性があり、代わりに上記のオブザーバー パターンをお勧めします。

サービスとクライアントが別々のプロセスにある場合、クライアントがアタッチしたイベント ハンドラーの数は、サービスに表示されません。 JsonRpc は常に、イベントをクライアントに伝達するハンドラーを 1 つだけアタッチします。 クライアントには、0 個以上のハンドラーが一番端にアタッチされていることがあります。

ほとんどの RPC クライアントでは、最初に接続されたときにイベント ハンドラーがワイヤードアップされません。 クライアントがインターフェイスで "Subscribe*" メソッドを呼び出してイベントを受信する関心と準備を示すまで、最初のイベントを発生させないでください。

イベントが状態の差分 (例えば、コレクションに新たな項目が追加された場合) を示す場合、クライアントがサブスクライブする際に過去のすべてのイベントをトリガーしたり、すべての現在データをイベント引数で新しいものとして記述したりすることで、イベント処理コードのみで "同期" を助けることを検討してください。

クライアントがデータまたは通知のサブセットに関心を示したい場合は、上記の "Subscribe*" メソッドで追加の引数を受け入れて、これらの通知を転送するために必要なネットワーク トラフィックと CPU を減らすことを検討してください。

変更通知を受け取るイベントも公開している場合、またはクライアントがイベントと組み合わせて使用することを積極的に推奨している場合は、現在の値を返すメソッドを提供しないことを検討してください。 データに対するイベントをサブスクライブして現在の値を取得するメソッドを呼び出すクライアントは、その値の変更と常に競争状態にあり、変更イベントを見逃したり、あるスレッドでの変更イベントを別のスレッドで取得された値と整合させる方法がわからない状況に直面する可能性があります。 この問題は、RPC 経由の場合だけでなく、あらゆるインターフェイスに対して一般的です。

名前付け規則

  • RPC インターフェイスで Service サフィックスと単純な I プレフィックスを使用します。
  • SDK のクラスには、Service サフィックスを使用しないでください。 ライブラリまたは RPC ラッパーでは、"service" という用語を避け、その動作を正確に説明する名前を使用する必要があります。
  • インターフェイス名またはメンバー名では"remote" という用語は使用しないでください。 ブローカー サービスは、リモートシナリオと同じ程度のローカル シナリオに理想的に適用されます。

バージョンの互換性に関する問題

他の拡張機能に公開される、または Live Share 経由で公開される特定のブローカー サービスに対して、前方互換性と下位互換性が必要です。つまり、クライアントがサービスよりも古いか新しい可能性があり、機能が 2 つの適用可能なバージョンのそれより小さいものとほぼ等しいと想定する必要があります。

まず、破壊的変更の用語を確認しましょう。

  • バイナリ破壊的変更: 以前のバージョンのアセンブリに対してコンパイルされた他のマネージド コードが、実行時に新しいものにバインドできない原因となる API の変更。 例を次に示します。

    • 既存のパブリック メンバーの署名を変更する。
    • パブリック メンバーの名前を変更する。
    • パブリック型を削除する。
    • 抽象メンバーを型に追加するか、インターフェイスに任意のメンバーを追加します。

    ただし、以下はバイナリの破壊的変更では "ありません"。

    • クラスまたは構造体への非抽象メンバーの追加。
    • 完全な (抽象ではない) インターフェイス実装を既存の型に追加する。
  • プロトコル破壊的変更: シリアル化された形式のデータ型または RPC メソッド呼び出しに対する変更。リモート パーティが正しく逆シリアル化して処理できないようにします。 例を次に示します。

    • RPC メソッドに必要なパラメーターを追加する。
    • 以前は null 以外であることが保証されていたデータ型からメンバーを削除する。
    • 他の既存の操作の前にメソッド呼び出しを行う必要がある要件を追加する。
    • そのメンバー内のデータのシリアル化された名前を制御するフィールドまたはプロパティの属性を追加、削除、または変更します。
    • (MessagePack): 既存のメンバーの DataMemberAttribute.Order プロパティまたは KeyAttribute 整数を変更します。

    ただし、以下はプロトコルの破壊的変更では "ありません"。

    • オプションのメンバーをデータ型に追加する。
    • RPC インターフェイスへのメンバーの追加。
    • 既存のメソッドに省略可能なパラメーターを追加する。
    • 整数または浮動小数点数を表すパラメーター型を、長さまたは精度の高いパラメーター型に変更します (たとえば、intlong に、floatdoubleに変更します)。
    • パラメーターの名前変更。 これは技術的には、JSON-RPC 名前付き引数を使用するクライアントに破壊的影響を与えますが、ServiceJsonRpcDescriptor を使用するクライアントは既定で位置引数を使用するため、パラメーター名の変更の影響を受けません。 クライアント のソースコード が名前付き引数構文を使用しているかどうかとは無関係であり、パラメーター名の変更は のソースを破壊する 変更になります。
  • 動作の破壊的変更: 古いクライアントが誤動作する可能性のある動作を追加または変更するブローカー サービスの実装に対する変更。 例を次に示します。

    • 以前は常に初期化されていたデータ型のメンバーを初期化しなくなりました。
    • 以前は正常に完了できた条件で例外をスローする。
    • 以前に返されたものとは異なるエラーコードを返します。

    ただし、以下は動作の破壊的変更では "ありません"。

    • 新しい例外の種類をスローする (すべての例外が RemoteInvocationException でラップされるため)。

破壊的変更が必要な場合は、新しいサービス モニカーを登録して提供することで、安全に変更を行うことができます。 このモニカーは同じ名前を共有できますが、バージョン番号は大きくなります。 バイナリの破壊的変更がない場合、元の RPC インターフェイス 再利用できる可能性があります。 それ以外の場合は、新しいサービス バージョンの新しいインターフェイスを定義します。 古いバージョンの登録、提供、サポートを続けることで、既存のクライアントへの影響を避けます。

RPC インターフェイスにメンバーを追加する以外は、このような破壊的変更をすべて回避したいと考えています。

RPC インターフェイスへのメンバーの追加

RPC クライアント コールバック インターフェイスにメンバーを追加 "しないでください"。多くのクライアントがこのインターフェイスを実装する場合があり、メンバーを追加したときに、それらの型が読み込まれたが、新しいインターフェイス メンバーが実装されないと、CLR で TypeLoadException がスローされるためです。 RPC クライアント コールバック ターゲットで呼び出すメンバーを追加する必要がある場合は、新しいインターフェイス (元のインターフェイスから派生する可能性があります) を定義してから、増分バージョン番号でブローカー サービスを提供するための標準プロセスに従い、更新されたクライアント インターフェイスの種類を指定して記述子を提供します。

仲介型サービスを定義する RPC インターフェイスにはメンバーを追加 "できます"。 これはプロトコル破壊的変更ではなく、サービスを実装するユーザーに対するバイナリ破壊的変更にすぎませんが、おそらく、新しいメンバーを実装するようにサービスを更新することになります。 私たちのガイダンス では、ブローカーサービス自体以外は誰もRPCインターフェイスを実装するべきではありません(テストではモックフレームワークを使用するべきです)。したがって、RPCインターフェイスにメンバーを追加しても誰にも影響を与えないはずです。

これらの新しいメンバーには、そのメンバーを最初に追加したサービス バージョンを識別する xml ドキュメント コメントが必要です。 新しいクライアントが、メソッドを実装していない古いサービスでメソッドを呼び出した場合、そのクライアントは RemoteMethodNotFoundExceptionをキャッチできます。 しかし、そのクライアントは失敗を予測し、そもそも呼び出しを回避することができ、おそらくそうするべきです。 既存のサービスにメンバーを追加するためのベスト プラクティスは次のとおりです。

  • これがサービスのリリース内の最初の変更である場合: メンバーを追加して新しい記述子を宣言するときに、サービス識別名のマイナーバージョンを更新します。
  • 古いバージョンに "加えて"、新しいバージョンを登録して提供するようにサービスを更新します。
  • ブローカー サービスのクライアントがある場合は、クライアントを更新して新しいバージョンを要求し、新しいバージョンが null として返された場合は、古いバージョンを要求するようにフォールバックします。