次の方法で共有


仲介型サービスを設計するためのベスト プラクティス

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

また、仲介型サービスには次のガイドラインが適用されます。

メソッド シグネチャ

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

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

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

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

メソッドの戻り値の型として、Task<T> ではなく ValueTask<T> を使用することを検討してください。ValueTask<T> の方が割り当てが少なくなるためです。 一般的ではない種類 (たとえば、TaskValueTask など) を使用する場合は、それほど重要ではありませんが、それでも ValueTask の方が好ましいと言えます。 その API に記載されているように、ValueTask<T> の使用に関する制限事項に注意してください。 このブログの投稿動画も、使用する種類を決定する際に役立ちます。

カスタム データ型

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

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

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

DiscriminatedTypeJsonConverter<TBase> を使用して、JSON を使用するポリモーフィックな型のシリアライズをサポートします。

コレクション

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

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

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

オブザーバー パターン

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

オブザーバー パターンは、以下のように単純なものにすることができます。

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

上記で使用した IDisposableIObserver<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 インターフェイスにメンバーを追加する。
    • 既存のメソッドに省略可能なパラメーターを追加する。
    • 整数または浮動小数点数を表すパラメーター型を、より長いかより精度が高いものに変更する (たとえば、int から long、または float から double)。
    • パラメーターの名前を変更する。 これは技術的には、JSON-RPC の名前付き引数を使用するクライアントにとって破壊的ですが、ServiceJsonRpcDescriptor を使用するクライアントは、既定では位置引数を使用するため、パラメーター名の変更による影響は受けません。 このことは、クライアント "ソース コード" が、名前付き引数構文 (パラメーター名の変更が "ソースの破壊的" 変更になります) を使用するかどうかには関係ありません。
  • 動作の破壊的変更: 古いクライアントが誤動作する可能性のある、動作を追加または変更する仲介型サービスの実装に対する変更。 以下に例を示します。

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

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

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

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

RPC インターフェイスへのメンバーの追加を除き、このような破壊的変更をすべて回避する必要があります。

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

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

仲介型サービスを定義する RPC インターフェイスにはメンバーを追加 "できます"。 これはプロトコルの破壊的変更ではなく、サービスの実装者に対するバイナリの破壊的変更にすぎません。ただし、おそらくこの場合も、新しいメンバーを実装するためにサービスを更新することになります。 このガイダンスでは、仲介型サービス自体以外は、RPC インターフェイスの実装は行わないため (テストではモック フレームワークを使用する必要があります)、RPC インターフェイスにメンバーを追加しても何も破損されません。

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

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