仲介型サービスを使用する
このドキュメントでは、仲介型サービスの取得、一般的な使用、廃棄に関連するすべてのコード、パターン、および注意について説明します。 取得した特定の仲介型サービスを使用する方法については、その仲介型サービスのドキュメントを参照してください。
このドキュメントのすべてのコードで、C# の null 許容参照型機能をアクティブ化することを強くお勧めします。
IServiceBroker の取得
仲介型サービスを取得するには、最初に IServiceBroker .のインスタンスが必要です。 コードを MEF (Managed Extensibility Framework) または VSPackage のコンテキストで実行する場合、通常はグローバル サービス ブローカーが必要になります。
仲介型サービス自体は、サービス ファクトリが呼び出されたときに割り当てられた IServiceBroker を使用する必要があります。
グローバル サービス ブローカー
Visual Studio には、グローバル サービス ブローカーを取得する 2 つの方法があります。
GlobalProvider.GetServiceAsync を使用して SVsBrokeredServiceContainer を要求する:
IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
IServiceBroker serviceBroker = container.GetFullAccessServiceBroker();
Visual Studio 2022 以降、MEF でアクティブ化された拡張機能で実行されるコードは、グローバル サービス ブローカーをインポートできます。
[Import(typeof(SVsFullAccessServiceBroker))]
IServiceBroker ServiceBroker { get; set; }
Import 属性の typeof
引数は必須であることに注意してください。
グローバル IServiceBroker に対する各要求によって作成される新しいオブジェクトのインスタンスは、グローバル仲介型サービス コンテナーへのビューとして機能します。 このサービス ブローカーの一意のインスタンスを使用すると、クライアントはそのクライアントの使用に固有の AvailabilityChanged イベントを受け取ることができます。 拡張機能内の各クライアント/クラスが、1 つのインスタンスを取得して拡張機能全体で共有するのでなく、上記のいずれかの方法を使用して独自のサービス ブローカーを取得するようにすることをお勧めします。 このパターンでは、仲介型サービスがグローバル サービス ブローカーを使用しないようにする、安全なコーディング パターンも推奨されます。
重要
通常、IServiceBroker の実装では IDisposable が実装されませんが、AvailabilityChanged ハンドラーが存在する間はこれらのオブジェクトを収集できません。 イベント ハンドラーの追加と削除のバランスを取るようにしてください (特に、プロセスの有効期間中にコードがサービス ブローカーを破棄する可能性がある場合)。
コンテキスト固有のサービス ブローカー
適切なサービス ブローカーを使用することが、特に Live Share セッションのコンテキストにおいては、仲介型サービスのセキュリティ モデルの重要な要件です。
仲介型サービスは自分の IServiceBroker でアクティブ化され、このインスタンスをその仲介型サービスのあらゆるニーズ (Proffer で提供されるサービスを含む) で使用する必要があります。 このようなコードが提供する BrokeredServiceFactory は、インスタンス化された仲介型サービスによって使用されるサービス ブローカーを受け取ります。
仲介型サービス プロキシの取得
仲介型サービスの取得は、一般には GetProxyAsync メソッドで行われます。
GetProxyAsync メソッドには ServiceRpcDescriptor と、ジェネリック型引数としてサービス インターフェイスが必要になります。 あなたが要求している仲介型サービスに関するドキュメントには、どこで記述子を取得するかとどのインターフェイスを使用するかが示されているはずです。 Visual Studio に含まれる仲介型サービスの場合、使用するインターフェイスは記述子の IntelliSense ドキュメントに示されます。 Visual Studio 仲介型サービスの記述子を見つける方法について、「使用可能な仲介型サービスの検出」を参照してください。
IServiceBroker broker; // Acquired as described earlier in this topic
IMyService? myService = await broker.GetProxyAsync<IMyService>(serviceDescriptor, cancellationToken);
using (myService as IDisposable)
{
Assumes.Present(myService); // Throw if service was not available
await myService.SayHelloAsync();
}
すべての仲介型サービス要求と同様に、上記のコードでは仲介型サービスの新しいインスタンスをアクティブ化しています。
サービスの使用後、上記のコードでは using
ブロックの終了時にプロキシが破棄されます。
重要
サービス インターフェイスが IDisposable から派生していなくても、取得したプロキシはすべて、破棄する必要があります。
破棄が重要である理由は、プロキシは I/O リソースをバックアップしているためにガベージがコレクションされないことが多いからです。
破棄によって I/O は終了し、プロキシはガベージ コレクションできるようになります。
IDisposable への条件付きキャストを使用して廃棄を行い、キャストの失敗に備えて、null
プロキシや IDisposable を実際に実装しないプロキシに関する例外を回避してください。
最新の Microsoft.ServiceHub.Analyzers NuGet パッケージをインストールし、このようなリークを防ぐために ISBxxxx アナライザー規則を有効にしておく必要があります。
プロキシを破棄すると、そのクライアント専用に割り当てられていた仲介型サービスも破棄されます。
仲介型サービスを必要とするコードで、仲介型サービスを利用できないために作業を完了できないときに、コードがユーザー エクスペリエンスを所有している場合は、例外をスローするのではなく、ユーザーにエラー ダイアログを表示できます。
クライアント RPC ターゲット
一部の仲介型サービスでは、"コールバック" 用にクライアント RPC (リモート プロシージャ コール) ターゲットを受け取るか要求します。このようなオプションまたは要件は、その仲介型サービスのドキュメントに記載されています。 Visual Studio 仲介型サービスの場合、この情報は記述子の IntelliSense ドキュメントに記載されているはずです。
このような場合、クライアントは次のように ServiceActivationOptions.ClientRpcTarget を使用して RPC ターゲットを提供できます。
IMyService? myService = await broker.GetProxyAsync<IMyService>(
serviceDescriptor,
new ServiceActivationOptions
{
ClientRpcTarget = new MyCallbackObject(),
},
cancellationToken);
クライアント プロキシの呼び出し
仲介型サービスを要求した結果は、プロキシによって実装されるサービス インターフェイスのインスタンスです。 このプロキシは呼び出しとイベントをそれぞれの方向に転送しますが、サービスを直接呼び出すときに期待される動作とは重要な違いがあります。
オブザーバー パターン
サービス コントラクトが IObserver<T> 型のパラメーターを受け取る場合、この型の構築方法の詳細については、「方法: オブザーバーを実装する」を参照してください。
IObserver<T> を AsObserver 拡張メソッドで実装するよう ActionBlock<TInput> を調整できます。 リアクティブ フレームワークの System.Reactive.Observer クラスは、インターフェイスを自分で実装するためのもう 1 つの選択肢です。
プロキシからスローされる例外
- 仲介型サービスからスローされた例外について RemoteInvocationException がスローされることを想定してください。 元の例外は InnerException にあります。
これがリモートでホストされるサービスの自然な動作である理由は、それが JsonRpc からの動作であるからです。
サービスがローカルのとき、ローカル プロキシはすべての例外を同じ方法でラップするため、クライアント コードはローカル サービスとリモート サービスで動作する例外パスを 1 つだけ持つことができます。
- サービス ドキュメントで、分岐できる特定条件に基づく特定コードの設定が推奨されている場合は、ErrorCode プロパティを確認します。
- より広範な一連のエラーを伝えるためにキャッチする RemoteRpcException は、RemoteInvocationException の基本型です。
- リモート サービスへの接続が切れたときやサービスをホストしているプロセスがクラッシュしたとき、ConnectionLostException が任意の呼び出しからスローされることを想定してください。 これは主に、サービスをリモートで取得できる場合に問題となります。
プロキシのキャッシュ
仲介型サービスと関連するプロキシのアクティブ化には、特にサービスがリモート プロセスから提供されるとき、いくらかの費用がかかります。
仲介型サービスを頻繁に使用することで、クラスへの多数の呼び出しにわたってプロキシをキャッシュする必要がある場合は、プロキシをそのクラスのフィールドに格納できます。
包含するクラスは使い捨てであり、その Dispose
メソッド内のプロキシを破棄する必要があります。
以下に例を示します。
class MyExtension : IDisposable
{
readonly IServiceBroker serviceBroker;
IMyService? serviceProxy;
internal MyExtension(IServiceBroker serviceBroker)
{
this.serviceBroker = serviceBroker;
}
async Task SayHiAsync(CancellationToken cancellationToken)
{
if (this.serviceProxy is null)
{
this.serviceProxy = await this.serviceBroker.GetProxyAsync<IMyService>(serviceDescriptor, cancellationToken);
Assumes.Present(this.serviceProxy);
}
await this.serviceProxy.SayHelloAsync();
}
public void Dispose()
{
(this.serviceProxy as IDisposable)?.Dispose();
}
}
上記は、ほぼ正しいコードですが、Dispose
と SayHiAsync
の間の競合状態は考慮されていません。
また、このコードでは、取得済みのプロキシの破棄と、次に必要になったときにプロキシの再取得につながる AvailabilityChanged イベントも考慮されていません。
ServiceBrokerClient クラスはこれらの競合状態や無効化状態を処理するように設計されており、あなたのコードをシンプルに保つのに役立ちます。 このヘルパー クラスを使用してプロキシをキャッシュする、次の更新された例を考えてみましょう。
class MyExtension : IDisposable
{
readonly ServiceBrokerClient serviceBrokerClient;
internal MyExtension(IServiceBroker serviceBroker)
{
this.serviceBrokerClient = new ServiceBrokerClient(serviceBroker);
}
async Task SayHiAsync(CancellationToken cancellationToken)
{
using var rental = await this.serviceBrokerClient.GetProxyAsync<IMyService>(descriptor, cancellationToken);
Assumes.Present(rental.Proxy); // Throw if service is not available
IMyService myService = rental.Proxy;
await myService.SayHelloAsync();
}
public void Dispose()
{
// Disposing the ServiceBrokerClient will dispose of all proxies
// when their rentals are released.
this.serviceBrokerClient.Dispose();
}
}
上記のコードでは、ServiceBrokerClient およびプロキシの各レンタルを破棄する必要があります。 プロキシの破棄と使用の間の競合状態は ServiceBrokerClient オブジェクトによって処理され、これによりキャッシュされた各プロキシの破棄が、そのオブジェクトの破棄時か、そのプロキシの最後のレンタルが解放されたときのうちいずれか遅いときに行なわれます。
ServiceBrokerClient
に関する重要な注意事項
ServiceBrokerClient がキャッシュされたプロキシのインデックスを、ServiceMoniker だけをベースにして作成します。 ServiceActivationOptions を渡してキャッシュ後のプロキシを既に使用可能な場合、キャッシュされたプロキシは ServiceActivationOptions を使用せずに返され、サービスからの予期しない動作が発生します。 このような場合は、IServiceBroker を直接使用することを検討してください。
ServiceBrokerClient.GetProxyAsync から取得した ServiceBrokerClient.Rental<T> をフィールドに格納しないでください。 プロキシは既に ServiceBrokerClient によりひとつのメソッドのスコープを超えてキャッシュされています。 プロキシの有効期間をより細かく制御したい場合、特に AvailabilityChanged イベントによる再取得の場合は、代わりに IServiceBroker を直接使用して、サービス プロキシをフィールドに格納します。
ServiceBrokerClient を、ローカル変数でなく、フィールドに作成して、格納します。 これをメソッド内でローカル変数として作成および使用しても、IServiceBroker を直接使用する方法より価値が高くなることはありません。ただしその場合は、1 つのオブジェクト (サービス) ではなく 2 つのオブジェクト (クライアントとレンタル) を破棄する必要があります。
IServiceBroker と ServiceBrokerClient のいずれかを選択
どちらも使いやすく、既定値はおそらく IServiceBroker になるでしょう。
カテゴリ | IServiceBroker | ServiceBrokerClient |
---|---|---|
使いやすい | はい | はい |
破棄が必要 | いいえ | はい |
プロキシの有効期間を管理する | いいえ。 使用後のプロキシは、所有者が処分しなければなりません。 | はい、有効期間中は存続し、再利用されます。 |
ステートレス サービスに適用される | はい | はい |
ステートフル サービスに適用される | はい | いいえ |
イベント ハンドラーがプロキシに追加されるとき適している | はい | いいえ |
古いプロキシが無効になったときに通知するイベント | AvailabilityChanged | Invalidated |
ServiceBrokerClient が提供するプロキシを迅速かつ頻繁に再利用するための便利な手段では、最上位レベルの操作の間に基礎となるサービスが変更されても気になりません。 ただし、もしあなたがそういったことを気にしていて、プロキシの有効期間を自分で管理したい場合や、イベント ハンドラーを必要とする (つまりプロキシの有効期間を管理する必要がある) 場合は、IServiceBroker を使用する必要があります。
サービスの中断に対する回復性
仲介型サービスでは、いくつかの種類のサービス中断が発生する可能性があります。
- サービスを利用できない。
- 以前に取得した仲介型サービスへの接続が切断された。
- サービスに対する詳細の要求が行われた場合にそのサービスの可用性に変更があった。
仲介型サービスのアクティブ化エラー
仲介型サービス要求を使用可能なサービスによって満たすことができるけれどもサービス ファクトリが未処理の例外をスローすると、ServiceActivationFailedException がクライアントにスロー バックされるため、クライアントはエラーを理解してユーザーに報告できます。
仲介型サービス要求が、利用可能なサービスと一致しない場合は、クライアントに null
が返されます。
このような場合は、そのサービスが後で利用可能になったときに AvailabilityChanged が発生します。
サービス要求が拒否されるのは、サービスが存在しないからではなく、提供されたバージョンが要求されたバージョンよりも低いためである可能性があります。 このためフォールバック計画には、クライアントが存在を認識していて対話が可能な下位バージョンでのサービス要求の再試行を含めることができます。
失敗したすべてのバージョン チェックの遅延が顕著になった場合、クライアントは VisualStudioServices.VS2019_4Services.RemoteBrokeredServiceManifest を要求して、どのサービスとバージョンをリモート ソースから利用できるかを調べることができます。
接続が切断された場合の処理
正常に取得された仲介型サービス プロキシが、接続の切断や、ホスト側プロセスのクラッシュによって失敗することがあります。 このような中断の後、何らかの呼び出しがそのプロキシで行われた結果として ConnectionLostException がスローされます。
仲介型サービス クライアントは、Disconnected イベントを処理することでこのような接続切断を事前に検出して対応できます。 このイベントに到達するためには、プロキシを IJsonRpcClientProxy にキャストして JsonRpc オブジェクトを取得する必要があります。 このキャストは、サービスがローカルであるときに正常に失敗するように、条件付きで行う必要があります。
if (this.myService is IJsonRpcClientProxy clientProxy)
{
clientProxy.JsonRpc.Disconnected += JsonRpc_Disconnected;
}
void JsonRpc_Disconnected(object? sender, JsonRpcDisconnectedEventArgs args)
{
if (args.Reason == DisconnectedReason.RemotePartyTerminated)
{
// consider reacquisition of the service.
}
}
サービス可用性の変更の処理
仲介型サービス クライアントは、以前に照会した仲介型サービスに対して再照会できるタイミングについての通知を受け取ることができます。これには、AvailabilityChanged イベントを処理します。 サービス要求の直後に発生したイベントが競合状態によって失われないようにするには、仲介型サービスを要求する前にこのイベントのハンドラーを追加する必要があります。
1 つの非同期メソッドの実行期間中のみを対象として仲介型サービスが要求される場合、このイベントを処理することはお勧めしません。 このイベントは、サービス変更に対応するためにプロキシを長期間保存し、プロキシを更新する立場にあるクライアントに最も適したものです。
このイベントは、どのようなスレッドでも発生させることができ、このイベントに記述されているサービスを使用するコードと同時に発生させることもできます。
このイベントは、次のような状態変化によって発生します。
- ソリューションまたはフォルダーが開かれているか、閉じられている。
- Live Share セッションが開始している。
- 動的に登録された仲介型サービスが検出されたばかりである。
影響を受ける仲介型サービスの結果としてこのイベントが生じるのは、以前にそのサービスを要求したクライアントだけで、その要求が満たされたかどうかは問いません。
このイベントは、1 サービスにつき最大 1 回だけ、そのサービスの各要求の後に発生します。 たとえば、クライアントがサービス A を要求し、サービス B で可用性の変更があった場合、そのクライアントにはイベントが発生しません。 その後、サービス A で可用性の変更が発生すると、クライアントはイベントを受け取ります。 クライアントがサービス A を再要求しない場合、その後生じた A の可用性の変化は、そのクライアントに通知されません。 クライアントが A をもう一度要求すると、そのサービスに関する次の通知を受け取れるようになります。
このイベントは、サービスが利用可能になったとき、利用できなくなったとき、または実装の変更が生じて先行するすべてのサービス クライアントによるサービスの再クエリが必要になったときに発生します。
ServiceBrokerClient はキャッシュされたプロキシに関する可用性変更イベントを自動的に処理するために、レンタルが返された時に古いプロキシを破棄し、サービスの新しいインスタンスを、その所有者がリクエストした場合に要求します。 このクラスを使用すると、サービスがステートレスであり、コードでプロキシにイベント ハンドラーをアタッチする必要がない場合に、コードを大幅に簡素化できます。
仲介型サービス パイプの取得
仲介型サービスにプロキシを介してアクセスするのが最も一般的で便利な手法ですが、高度なシナリオでは、クライアントが RPC を直接制御したり他のデータ型を直接通信したりできるように、そのサービスへのパイプを要求することが望ましい場合や必要な場合があります。
仲介型サービスへのパイプは、GetPipeAsync メソッドを介して取得できます。 このメソッドが ServiceMoniker を ServiceRpcDescriptor の代わりに使用するのは、記述子によって提供される RPC 動作が必要でないからです。 記述子があるときは、そこからモニカーを ServiceRpcDescriptor.Moniker プロパティを介して取得できます。
パイプは I/O にバインドされていますが、ガベージ コレクションの対象にはなりません。 メモリ リークを回避するために、これらのパイプは、使われなくなったら必ず終了させてください。
次のスニペットでは、仲介型サービスがアクティブ化され、クライアントはそこに直接パイプを設けます。 その後、クライアントはファイルの内容をサービスに送信して、切断します。
async Task SendMovieAsync(string movieFilePath, CancellationToken cancellationToken)
{
IServiceBroker serviceBroker;
IDuplexPipe? pipe = await serviceBroker.GetPipeAsync(serviceMoniker, cancellationToken);
if (pipe is null)
{
throw new InvalidOperationException($"The brokered service '{serviceMoniker}' is not available.");
}
try
{
// Open the file optimized for async I/O
using FileStream fs = new FileStream(movieFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, bufferSize: 4096, useAsync: true);
await fs.CopyToAsync(pipe.Output.AsStream(), cancellationToken);
}
catch (Exception ex)
{
// Complete the pipe, passing through the exception so the remote side understands what went wrong.
await pipe.Input.CompleteAsync(ex);
await pipe.Output.CompleteAsync(ex);
throw;
}
finally
{
// Always complete the pipe after successfully using the service.
await pipe.Input.CompleteAsync();
await pipe.Output.CompleteAsync();
}
}
仲介型サービス クライアントのテスト
仲介型サービスは、拡張機能をテストする際にモックするのに適した依存関係です。 仲介型サービスをモックする際に、あなたの代わりにインターフェイスを実装し、あなたのクライアントが呼び出す特定のメンバーにあなたが必要とするコードを注入するモック フレームワークを使用することをお勧めします。 これにより、仲介型サービス インターフェイスにメンバーが追加されても、テストは中断することなくコンパイルと実行を継続することができます。
Microsoft.VisualStudio.Sdk.TestFramework を使用して拡張機能をテストするとき、テストには、クライアント コードが照会して実行できるモック サービスを提供するための標準コードを含めることができます。 たとえば、VisualStudioServices.VS2022.FileSystem 仲介型サービスをテストでモックアップするとします。 モックを次のコードで提供することができます。
IBrokeredServiceContainer sbc = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
Mock<IFileSystem> mockFileSystem = new Mock<IFileSystem>();
sbc.Proffer(VisualStudioServices.VS2022.FileSystem, (ServiceMoniker moniker, ServiceActivationOptions options, IServiceBroker serviceBroker, CancellationToken cancellationToken) => new ValueTask<object?>(mockFileSystem.Object));
モックされた仲介型サービス コンテナーでは、Visual Studio のように、提供されるサービスを先に登録する必要はありません。
テスト対象のコードは仲介型サービスを通常どおり取得できますが、テスト下では、Visual Studio で実行中に取得される実際のサービスではなく、モックが取得されます。
IBrokeredServiceContainer sbc = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
IServiceBroker serviceBroker = sbc.GetFullAccessServiceBroker();
IFileSystem? proxy = await serviceBroker.GetProxyAsync<IFileSystem>(VisualStudioServices.VS2022.FileSystem);
using (proxy as IDisposable)
{
Assumes.Present(proxy);
await proxy.DeleteAsync(new Uri("file://some/file"), recursive: false, null, this.TimeoutToken);
}