仲介型サービスを提供する
仲介型サービスは次の要素で構成されます。
- サービスの機能を宣言し、サービスとそのクライアント間のコントラクトとして機能するインターフェイス。
- そのインターフェイスの実装。
- 名前とバージョンをサービスに割り当てるサービス モニカー。
- 必要に応じて RPC (リモート プロシージャ コール) を処理するための動作とサービス モニカーを組み合わせる記述子。
- サービス ファクトリを提供して仲介型サービスを VS パッケージに登録するか、両方を MEF (Managed Extensibility Framework) で実行します。
上のリストに含まれる各項目については、次のセクションで詳しく説明します。
この記事のすべてのコードで、C# の null 許容参照型機能をアクティブ化することを強くお勧めします。
サービス インターフェイス
サービス インターフェイスは、(多くの場合 C# で記述された) 標準の .NET インターフェイスにすることができます。ただし、クライアントとサービスが異なるプロセスで実行されるときに RPC 経由でインターフェイスを確実に使用できるようにするため、サービスで使用する ServiceRpcDescriptor 派生型によって設定されたガイドラインに従う必要があります。
通常、これらの制限にはそのプロパティが含まれており、インデクサーは許可されません。ほとんどまたはすべてのメソッドによって、Task
または非同期互換の別の戻り値の型が返されます。
ServiceJsonRpcDescriptor は、仲介型サービスに推奨される派生型です。 このクラスでは、クライアントとサービスの通信に RPC が必要な場合に StreamJsonRpc ライブラリを利用します。 ここで説明しているように、StreamJsonRpc によって、サービス インターフェイスに対して特定の制約が適用されます。
インターフェイスは、IDisposable、System.IAsyncDisposable、または場合によっては Microsoft.VisualStudio.Threading.IAsyncDisposable からする "ことができます" が、これはシステムで必要ではありません。 生成されたクライアント プロキシでは、どちらの方法でも IDisposable が実装されます。
単純な電卓サービス インターフェイスは、次のように宣言できます。
public interface ICalculator
{
ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken);
ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken);
}
このインターフェイスのメソッドの実装では非同期メソッドが保証されない場合がありますが、このインターフェイスでは常に非同期メソッド シグネチャを使用します。このインターフェイスは、このサービスをリモートで呼び出す可能性のあるクライアント プロキシを生成するために使用され、これによって非同期メソッド シグネチャが間違いなく保証 "される" ためです。
インターフェイスでは、サービスで発生するイベントをクライアントに通知するために使用できるイベントを宣言できます。
クライアントに "コールバック" する必要がある仲介型サービスでは、イベントやオブザーバーの設計パターン以外に、サービスを要求するときに ServiceActivationOptions.ClientRpcTarget プロパティを使用してクライアントで実装して提供する必要があるコントラクトとして機能する、2 つ目のインターフェイスを定義することがあります。 このようなインターフェイスでは、仲介型サービス インターフェイスと同じ設計パターンと制限すべてに準拠している必要がありますが、バージョン管理に関する制限が追加されています。
パフォーマンスの高い将来性のある RPC インターフェイスの設計に関するヒントについては、「仲介型サービスを設計するためのベスト プラクティス」を参照してください。
このインターフェイスは、サービスを実装するアセンブリとは異なるアセンブリで宣言すると便利です。これによりそのクライアントでは、実装の詳細をサービスで公開しなくても、インターフェイスを参照できます。 また、他の拡張機能で参照する NuGet パッケージとしてインターフェイス アセンブリを出荷する一方、サービス実装を出荷するために独自の拡張機能を予約する場合にも役立ちます。
.NET Framework、.NET Core、.NET 5 以降のいずれを実行しているかにかかわらず、サービスを任意の .NET プロセスから簡単に呼び出すことができるように、サービス インターフェイスを netstandard2.0
に宣言するアセンブリを対象にすることを検討します。
テスト
インターフェイスの RPC 対応性を確認するには、サービス "インターフェイス" と共に自動テストを記述する必要があります。
テストでは、インターフェイスを介して渡されたすべてのデータがシリアル化可能であることを確認する必要があります。
Microsoft.VisualStudio.Sdk.TestFramework.Xunit パッケージの BrokeredServiceContractTestBase<TInterface,TServiceMock> クラスは、インターフェイス テスト クラスを派生させるために便利な場合があります。 このクラスには、インターフェイスの基本的な規則テスト、イベント テストなどの一般的なアサーションに役立つメソッドなどが含まれています。
メソッド
すべての引数と戻り値が完全にシリアル化されたことをアサートします。 さきほど説明したテスト ベース クラスを使用していた場合、コードは次のようになります。
public interface IYourService
{
Task<bool> SomeOperationAsync(YourStruct arg1);
}
public static class Descriptors
{
public static readonly ServiceRpcDescriptor YourService = new ServiceJsonRpcDescriptor(
new ServiceMoniker("YourCompany.YourExtension.YourService", new Version(1, 0)),
clientInterface: null,
ServiceJsonRpcDescriptor.Formatters.MessagePack,
ServiceJsonRpcDescriptor.MessageDelimiters.BigEndianInt32LengthHeader,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 })
.WithExceptionStrategy(StreamJsonRpc.ExceptionProcessing.ISerializable);
}
public class YourServiceMock : IYourService
{
internal YourStruct? SomeOperationArg1 { get; set; }
public Task<bool> SomeOperationAsync(YourStruct arg1, CancellationToken cancellationToken)
{
this.SomeOperationArg1 = arg1;
return true;
}
}
public class BrokeredServiceTests : BrokeredServiceContractTestBase<IYourService, YourServiceMock>
{
public BrokeredServiceTests(ITestOutputHelper logger)
: base(logger, Descriptors.YourService)
{
}
[Fact]
public async Task SomeOperation()
{
var arg1 = new YourStruct
{
Field1 = "Something",
};
Assert.True(await this.ClientProxy.SomeOperationAsync(arg1, this.TimeoutToken));
Assert.Equal(arg1.Field1, this.Service.SomeOperationArg1.Value.Field1);
}
}
同じ名前の複数のメソッドを宣言する場合は、オーバーロードの解決をテストすることを検討してください。
テスト メソッドでメソッドを呼び出してから、正しいメソッドが正しい引数で呼び出されたことを確認できるように、そのメソッドの引数を格納するモック サービスのメソッドごとに、internal
フィールドをそのサービスに追加できます。
Events
インターフェイスで宣言されたイベントでは、RPC 対応性もテストする必要があります。 仲介型サービスから発生したイベントは "ファイア アンド フォーゲット型" であるため、RPC のシリアル化中に失敗してもテストの失敗にはなりません。
さきほど説明したテスト ベース クラスを使用していた場合、この動作は既にいくつかのヘルパー メソッドに組み込まれており、次のようになります (簡潔にするため、変わっていない部分は省略しています)。
public interface IYourService
{
event EventHandler<int> NewTotal;
}
public class YourServiceMock : IYourService
{
public event EventHandler<int>? NewTotal;
internal void RaiseNewTotal(int arg) => this.NewTotal?.Invoke(this, arg);
}
public class BrokeredServiceTests : BrokeredServiceContractTestBase<IYourService, YourServiceMock>
{
[Fact]
public async Task NewTotal()
{
await this.AssertEventRaisedAsync<int>(
(p, h) => p.NewTotal += h,
(p, h) => p.NewTotal -= h,
s => s.RaiseNewTotal(50),
a => Assert.Equal(50, a));
}
}
サービスの実装
サービス クラスでは、前の手順で宣言した RPC インターフェイスを実装する必要があります。 サービスでは、IDisposable か、RPC に使用されるもの以外の他のインターフェイスを実装できます。 クライアントで生成されたプロキシでは、サービス インターフェイス IDisposable のほか、システムをサポートするための限定されたインターフェイスのみが (必要に応じて) 実装されるため、サービスによって実装された他のインターフェイスへのキャストは、クライアント上では失敗になります。
ここで実装する、上で使用した電卓の例を考えてみましょう。
internal class Calculator : ICalculator
{
public ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken)
{
return new ValueTask<double>(a + b);
}
public ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken)
{
return new ValueTask<double>(a - b);
}
}
メソッド本体は非同期である必要がないため、サービス インターフェイスに準拠するために、構築された戻り型 ValueTask<TResult> で戻り値を明示的にラップします。
監視可能な設計パターンの実装
サービス インターフェイスでオブザーバー サブスクリプションを指定すると、次のようになります。
Task<IDisposable> SubscribeAsync(IObserver<YourDataType> observer);
IObserver<T> 引数では通常、このメソッド呼び出しの有効期間を長くする必要があります。これは、返される IDisposable 値をクライアントで破棄するまで、メソッド呼び出しが完了した後もクライアントで更新を引き続き受け取ることができるようにするためです。 これを容易にするため、状態に対して行われた更新によってすべてのサブスクライバーを更新するために列挙される、IObserver<T> サブスクリプションのコレクションがサービス クラスに含まれている場合があります。 コレクションの列挙は、相互に、特に追加のサブスクリプションまたはそれらのサブスクリプションの破棄によって発生する可能性があるそのコレクションの変更に関してスレッド セーフであるようにしてください。
OnNext 経由で投稿されたすべての更新では、サービスに状態変更が導入された順序が保持されるように注意してください。
すべてのサブスクリプションは最終的には、クライアントおよび RPC システムでのリソース リークを回避するために、OnCompleted または OnError のいずれかの呼び出しで終了する必要があります。 これには、残りすべてのサブスクリプションを明示的に完了する必要があるサービス破棄が含まれます。
オブザーバーの設計パターン、監視可能なデータ プロバイダーを実装する方法について、特に RPC を念頭に置いて詳細を確認します。
破棄可能なサービス
サービス クラスは破棄可能である必要はありませんが、破棄可能なサービスは、クライアントでサービスに対するプロキシが破棄されるか、クライアントとサービスの間の接続が失われたときに破棄されます。 破棄可能なインターフェイスは、System.IAsyncDisposable、Microsoft.VisualStudio.Threading.IAsyncDisposable、IDisposable の順序でテストされます。 サービス クラスで実装するこの一覧の最初のインターフェイスのみが、サービスの破棄に使用されます。
破棄を検討するときは、スレッドセーフを意識してください。 サービス内の他のコードが実行されている間 (たとえば、接続が切断された場合) は、任意のスレッドで Dispose メソッドが呼び出される可能性があります。
例外のスロー
例外をスローする場合は、RemoteInvocationException でクライアントで受け取ったエラー コードを制御するために、特定の ErrorCode を指定して LocalRpcException をスローすることを検討します。 クライアントにエラー コードを提供すると、例外メッセージや型を解析するよりも効果的に、エラーの性質に基づいた分岐が可能になります。
JSON-RPC 仕様に従うと、エラー コードは -32000 (正の数値を含む) より大きくする必要があります。
他の仲介型サービスの使用
仲介型サービス自体から別の仲介型サービスにアクセスする必要がある場合は、サービス ファクトリに提供される IServiceBroker を使用することをお勧めします。ただし、仲介型サービス登録で AllowTransitiveGuestClients フラグが設定されている場合、これは特に重要です。
電卓サービスで動作を実装するために他の仲介型サービスが必要な場合にこのガイドラインに準拠させるには、IServiceBroker を受け入れるようにコンストラクターを変更します。
internal class Calculator : ICalculator
{
private readonly State state;
private readonly IServiceBroker serviceBroker;
internal class Calculator(State state, IServiceBroker serviceBroker)
{
this.state = state;
this.serviceBroker = serviceBroker;
}
// ...
}
仲介型サービスをセキュリティで保護し、仲介型サービスを使用する方法の詳細について説明します。
ステートフル サービス
クライアントごとの状態
このクラスの新しいインスタンスは、サービスを要求するクライアントごとに作成されます。
上記の Calculator
クラスのフィールドには、各クライアントに固有の値が格納されます。
操作が実行されるたびに増分するカウンターを追加するとします。
internal class Calculator : ICalculator
{
int operationCounter;
public ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken)
{
this.operationCounter++;
return new ValueTask<double>(a + b);
}
public ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken)
{
this.operationCounter++;
return new ValueTask<double>(a - b);
}
}
仲介型サービスは、スレッドセーフなプラクティスに従って記述する必要があります。
推奨される ServiceJsonRpcDescriptor を使用する場合、このドキュメントで説明されているように、クライアントとのリモート接続にはサービスのメソッドの同時実行を含めることができます。
クライアントでプロセスと AppDomain をサービスと共有する場合、クライアントでは複数のスレッドからサービスを同時に呼び出す可能性があります。
上記の例のスレッドセーフな実装では、Interlocked.Increment(Int32) を使用して operationCounter
フィールドを増分することができます。
共有状態
すべてのクライアント間でサービスによって共有する必要がある状態がある場合、この状態は、VS パッケージによってのインスタンスが作成され、サービスのコンストラクターに引数として渡される個別のクラスで定義する必要があります。
上記で定義した operationCounter
によって、サービスのすべてのクライアントですべての操作をカウントするとします。
フィールドをこの新しい状態クラスに移行する必要があります。
internal class Calculator : ICalculator
{
private readonly State state;
internal Calculator(State state)
{
this.state = state;
}
public ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken)
{
this.state.IncrementCounter();
return new ValueTask<double>(a + b);
}
public ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken)
{
this.state.IncrementCounter();
return new ValueTask<double>(a - b);
}
internal class State
{
private int operationCounter;
internal int OperationCounter => this.operationCounter;
internal void IncrementCounter() => Interlocked.Increment(ref this.operationCounter);
}
}
これで、Calculator
サービスの複数のインスタンスで共有状態を管理するための、洗練されテスト可能な方法が用意されました。
後でサービスを提供するコードを記述するときに、この State
クラスが 1 回作成され、Calculator
サービスのすべてのインスタンスと共有される方法がわかります。
共有状態を処理する場合は、スレッドセーフであることが特に重要です。これは、呼び出しが同時に行われることが絶対にないように複数のクライアントでスケジュールされると仮定できないためです。
共有状態クラスから他の仲介型サービスにアクセスする必要がある場合は、仲介型サービスの個々のインスタンスに割り当てられたコンテキスト サービス ブローカーの 1 つではなく、グローバル サービス ブローカーを使用する必要があります。 仲介型サービス内でグローバル サービス ブローカーを使用すると、ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients フラグをい設定したときにセキュリティーへの影響があります。
セキュリティに関する考慮事項
ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients フラグを使用して仲介型サービスが登録されている場合、セキュリティが考慮事項となります。これによって、共有の Live Share セッションに参加している他のマシン上の他のユーザーからアクセスできるように公開されます。
仲介型サービスをセキュリティで保護する方法に関する記事を確認し、AllowTransitiveGuestClients フラグを設定する前に必要なセキュリティ軽減策を講じてください。
サービス モニカー
仲介型サービスには、クライアントでサービスを要求できるシリアル化可能な名前とバージョン (省略可能) が必要です。 ServiceMoniker は、これら 2 つの情報の便利なラッパーです。
サービス モニカーは、CLR (共通言語ランタイム) 型のアセンブリ修飾フル ネームに似ています。 グローバルに一意である必要があるため、会社名とおそらく拡張機能名をサービス名自体のプレフィックスとして含める必要があります。
他の場所で使用するために、static readonly
フィールドでこのモニカーを定義すると便利な場合があります。
public static readonly ServiceMoniker Moniker = new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0"));
サービスのほとんどの用途ではモニカーを直接使用できませんが、プロキシではなくパイプ経由で通信するクライアントにはモニカーが必要です。
モニカーのバージョンは省略可能ですが、サービスの動作を変更した場合にクライアントとの互換性を維持するために利用できるオプションが多くなるため、バージョンを指定することをお勧めします。
サービス記述子
サービス記述子では、RPC 接続を実行してローカルまたはリモート プロキシを作成するために必要な動作をサービス モニカーと組み合わせます。 記述子は、RPC インターフェイスをワイヤ プロトコルに効果的に変換する役割を担います。 このサービス記述子は、ServiceRpcDescriptor 派生型のインスタンスです。 このサービスにアクセスするためにプロキシを使用するすべてのクライアントで記述子を使用できるようにする必要があります。 サービスを提供するには、この記述子も必要です。
Visual Studio では、このような派生型を 1 つ (ServiceJsonRpcDescriptor) 定義し、すべてのサービスで使用することをお勧めします。 この記述子では RPC 接続に StreamJsonRpc を利用し、RemoteInvocationException のサービスによってスローされる例外をラップするなど、リモート動作の一部をエミュレートする高パフォーマンスのローカル プロキシをローカル サービス用に作成します。
ServiceJsonRpcDescriptor では、JSON-RPC プロトコルの JSON または MessagePack エンコード用に JsonRpc クラスを構成することがサポートされています。 MessagePack エンコードをお勧めします。よりコンパクトで、パフォーマンスが 10 倍になる可能性があるためです。
次のように電卓サービスの記述子を定義できます。
/// <summary>
/// The descriptor for the calculator brokered service.
/// Use the <see cref="ICalculator"/> interface for the client proxy for this service.
/// </summary>
public static readonly ServiceRpcDescriptor CalculatorService = new ServiceJsonRpcDescriptor(
Moniker,
Formatters.MessagePack,
MessageDelimiters.BigEndianInt32LengthHeader,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 })
.WithExceptionStrategy(StreamJsonRpc.ExceptionProcessing.ISerializable);
上に示すように、任意のフォーマッタと区切り記号が使用可能です。 すべての組み合わせが有効なわけではないので、次のいずれかの組み合わせをお勧めします。
ServiceJsonRpcDescriptor.Formatters | ServiceJsonRpcDescriptor.MessageDelimiters | 最適な用途 |
---|---|---|
MessagePack | BigEndianInt32LengthHeader | 高性能 |
UTF8 (JSON) | HttpLikeHeaders | 他の JSON-RPC システムとの相互運用 |
MultiplexingStream.Options
オブジェクトを最後のパラメーターとして指定すると、クライアントとサービスの間で共有される RPC 接続は、MultiplexingStream 上のただ 1 つのチャネルになります。これは、JSON-RPC 経由で大きなバイナリ データを効率的に転送できるようにするために JSON-RPC 接続と共有されます。
ExceptionProcessing.ISerializable の戦略により、サービスからスローされた例外がシリアル化され、クライアントでスローされた RemoteInvocationException への Exception.InnerException として保持されます。 この設定がないと、クライアントで使用できる例外情報の詳細度が低くなります。
ヒント: 実装の詳細として使用する派生型ではなく、記述子を ServiceRpcDescriptor として公開します。 これにより、API の破壊的変更を発生させずに、実装の詳細を後で変更する高い柔軟性が得られます。
ユーザーがサービスを使用しやすくするために、記述子の xml ドキュメント コメントにサービス インターフェイスへの参照を含めます。 必要に応じて、サービスでクライアント RPC ターゲットとして受け入れるインターフェイスも参照します。
さらに高度なサービスによっては、一部のインターフェイスに準拠するクライアントから RPC ターゲット オブジェクトを受け入れるか、必要とする場合もあります。
このような場合は、Type clientInterface
パラメーターを指定して ServiceJsonRpcDescriptor コンストラクターを使用して、クライアントでインスタンスを指定する必要があるインターフェイスを指定します。
記述子のバージョン管理
時間の経過に伴い、サービスのバージョンを増分させることができます。 このような場合は、サポートするバージョンごとに一意のバージョン固有の ServiceMoniker を使用して記述子を定義する必要があります。 複数のバージョンを同時にサポートすることは下位互換性のために便利であり、通常は 1 つの RPC インターフェイスだけで実行できます。
Visual Studio では、元の ServiceRpcDescriptor を、その仲介型サービスを追加した最初のリリースを表す入れ子になったクラスの virtual
プロパティとして定義することによって、その VisualStudioServices クラスでこのパターンに従います。
ワイヤ プロトコルを変更したり、サービスの機能を追加/変更したりする必要がある場合、Visual Studio では、新しい ServiceRpcDescriptor を返す、後からバージョン管理され入れ子にされたクラスで override
プロパティを宣言します。
Visual Studio 拡張機能によって定義および提供されるサービスの場合、元の記述子プロパティの横で別のものを宣言するだけで十分なことがあります。 たとえば、1.0 サービスで UTF8 (JSON) フォーマッタが使用され、MessagePack に切り替えるとパフォーマンス上の大きな利点が得られることがわかったとします。 フォーマッタの変更はワイヤ プロトコルの破壊的変更であるため、仲介型サービスのバージョン番号と 2 番目の記述子を増分する必要があります。 2 つの記述子は合わせると次のようになります。
public static readonly ServiceJsonRpcDescriptor CalculatorService = new ServiceJsonRpcDescriptor(
new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0")),
Formatters.UTF8,
MessageDelimiters.HttpLikeHeaders,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 })
);
public static readonly ServiceJsonRpcDescriptor CalculatorService1_1 = new ServiceJsonRpcDescriptor(
new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.1")),
Formatters.MessagePack,
MessageDelimiters.BigEndianInt32LengthHeader,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 });
2 つの記述子を宣言しています (また、後で 2 つのサービスを提供して登録する必要があります) が、1 つのサービス インターフェイスと実装だけでこれを行い、複数のサービス バージョンをサポートするためのオーバーヘッドを非常に低く保つことができます。
サービスの提供
仲介型サービスは、要求が到着したときに作成する必要があります。これは、サービスの提供と呼ばれる手順を介して配置されます。
サービス ファクトリ
GlobalProvider.GetServiceAsync を使用して SVsBrokeredServiceContainer を要求します。 その後、そのコンテナーで IBrokeredServiceContainer.Proffer を呼び出して、サービスを提供します。
次の例では、ServiceRpcDescriptor のインスタンスに設定される、前に宣言した CalculatorService
フィールドを使用してサービスを提供します。
これを、BrokeredServiceFactory デリゲートであるサービス ファクトリに渡します。
IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
container.Proffer(
CalculatorService,
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService()));
通常、仲介型サービスのインスタンスはクライアントごとに 1 回作成されます。 これは、通常は 1 回インスタンス化され、すべてのクライアントで共有される他の VS (Visual Studio) サービスとは異なります。 クライアントごとにサービスのインスタンスを 1 つ作成すると、セキュリティが向上します。これは、各サービスとその接続によって、クライアントが動作する認可レベルや、その優先される CultureInfo などに関するクライアントごとの状態を保持できるためです。次に示すように、この要求に固有の引数を受け取る、より興味深いサービスも可能になります。
重要
このガイドラインから逸脱しており、新しいサービス インスタンスではなく共有のものを各クライアントに返すサービス ファクトリでは、そのサービスによる IDisposable の実装を "絶対に行わないでください"。そのプロキシを破棄する最初のクライアントによって、他のクライアントで使用を終了する前に共有サービス インスタンスが破棄されるためです。
CalculatorService
コンストラクターが共有状態オブジェクトと IServiceBroker を必要とするより高度なケースでは、次のようにファクトリを提供する場合があります。
var state = new CalculatorService.State();
container.Proffer(
CalculatorService,
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService(state, serviceBroker)));
state
ローカル変数はサービス ファクトリの外部にあるため、1 回だけ作成され、インスタンスが作成されたすべてのサービス間で共有されます。
さらに高度なのものでは、(たとえば、クライアント RPC ターゲット オブジェクトでメソッドを呼び出すために) サービスで ServiceActivationOptions へのアクセスが必要な場合、これも同様に渡されることがあります。
var state = new CalculatorService.State();
container.Proffer(CalculatorService, (moniker, options, serviceBroker, cancellationToken) =>
new ValueTask<object?>(new CalculatorService(state, serviceBroker, options)));
この場合、コンストラクター引数の 1 つとして typeof(IClientCallbackInterface)
を指定して ServiceJsonRpcDescriptor が作成されたと仮定すると、サービス コンストラクターは次のようになります。
internal class Calculator(State state, IServiceBroker serviceBroker, ServiceActivationOptions options)
{
this.state = state;
this.serviceBroker = serviceBroker;
this.options = options;
this.clientCallback = (IClientCallbackInterface)options.ClientRpcTarget;
}
これで、接続が破棄されるまで、サービスでクライアントを呼び出す必要があるときはいつでもこの clientCallback
フィールドを呼び出すことができるようになります。
サービス ファクトリが、モニカーに基づいて複数のサービスまたは異なるサービス バージョンを作成する共有メソッドである場合、BrokeredServiceFactory デリゲートはパラメーターとして ServiceMoniker を受け取ります。 このモニカーはクライアントから取得され、期待されるサービスのバージョンが含まれています。 このモニカーをサービス コンストラクターに転送することで、サービスでは特定のサービス バージョンの予測できない動作をエミュレートして、クライアントで期待されるものと一致させることができます。
仲介型サービス クラス内で IAuthorizationService を使用する場合を除き、IBrokeredServiceContainer.Proffer メソッドで AuthorizingBrokeredServiceFactory デリゲートを使用することは避けてください。 メモリ リークを避けるために、この IAuthorizationService は仲介型サービス クラスで破棄 する必要があります。
サービスの複数バージョンのサポート
ServiceMoniker でバージョンを増分する場合は、クライアント要求に応答する仲介型サービスの各バージョンを指定する必要があります。 これは、引き続きサポートする各 ServiceRpcDescriptor で IBrokeredServiceContainer.Proffer メソッドを呼び出すことによって行われます。
null
バージョン付きで提供したサービスは、"キャッチ オール" として機能します。この場合は、登録されたサービスとの正確なバージョン一致が存在しなくても、すべてのクライアント要求に対して一致として処理されます。
たとえば、1.0 と 1.1 のサービスを特定のバージョンで提供し、このサービスを null
バージョンで登録することもできます。
このような場合、1.0 または 1.1 でサービスを要求するクライアントでは、それらのバージョン用に提供されたサービス ファクトリが呼び出されますが、バージョン 8.0 を要求するクライアントでは、null バージョン付きで提供されたサービス ファクトリが呼び出されます。
クライアントが要求したバージョンがサービス ファクトリに提供されるため、ファクトリは、この特定のクライアントに対してサービスを構成する方法や、サポートされていないバージョンであることを示すために null
を返すかどうかを決定できます。
null
バージョン付きのサービスに対するクライアント要求は、null
バージョン付きで登録および提供されたサービスにのみ一致します。
サービスのバージョンを多数公開しており、そのうちのいくつかは下位互換性があり、サービス実装を共有している可能性がある場合について考えてみましょう。 次のように、キャッチ オール オプションを利用すると、個々のバージョンを繰り返し提示する必要がなくなります。
const string ServiceName = "YourCompany.Extension.Calculator";
ServiceRpcDescriptor CreateDescriptor(Version? version) =>
new ServiceJsonRpcDescriptor(
new ServiceMoniker(ServiceName, version),
ServiceJsonRpcDescriptor.Formatters.MessagePack,
ServiceJsonRpcDescriptor.MessageDelimiters.BigEndianInt32LengthHeader);
IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
container.Proffer(
CreateDescriptor(new Version(2, 0)),
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorServiceV2()));
container.Proffer(
CreateDescriptor(null), // proffer a catch-all
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(moniker.Version switch {
{ Major: 1 } => new CalculatorService(), // match any v1.x request with our v1 service.
null => null, // We don't support clients that do not specify a version.
_ => null, // The client requested some other version we don't recognize.
}));
サービスの登録
グローバル仲介型サービス コンテナーに仲介型サービスを提供すると、サービスが最初に登録されていた場合を除きスローされます。 提供コードを実行するために、使用可能な可能性がある仲介型サービスと、これらが要求されたときに読み込む VS パッケージをコンテナーで事前に把握する手段が登録によって提供されます。 これにより、すべての拡張機能を事前に読み込むことなく、仲介型サービスのクライアントから要求されたときに必要な拡張機能は読み込んで、Visual Studio を迅速に起動できます。
AsyncPackage 派生クラスに ProvideBrokeredServiceAttribute を適用することで登録することができます。 これは、ServiceAudience を設定できる唯一の場所です。
[ProvideBrokeredService("YourCompany.Extension.Calculator", "1.0", Audience = ServiceAudience.Local)]
既定値 Audience は ServiceAudience.Process で、仲介型サービスを同じプロセス内の他のコードにのみ公開します。 ServiceAudience.Local を設定すると、同じ Visual Studio セッションに属する他のプロセスに仲介型サービスを公開することが選択されます。
仲介型サービスを Live Share ゲストに公開する "必要がある" 場合は、Audience に ServiceAudience.LiveShareGuest を含めて、ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients プロパティは true
に設定する必要があります。
これらのフラグを設定すると、セキュリティ上の重大な脆弱性が生じる可能性があります。仲介型サービスをセキュリティで保護する方法のガイダンスに先に従っていない場合は、設定しないでください。
ServiceMoniker でバージョンを増分する場合は、クライアント要求に応答する仲介型サービスの各バージョンを登録する必要があります。 仲介型サービスの最新バージョンを超えてサポートすることで、前の仲介型サービス バージョンのクライアントの下位互換性を維持するのに役立ちます。これは、セッションを共有している Visual Studio の各バージョンが異なるバージョンである可能性がある、Live Share シナリオを検討する場合に特に便利です。
null
バージョン付きで登録したサービスは、"キャッチ オール" として機能します。この場合は、登録されたサービスとの正確なバージョン一致が存在しなくても、すべてのクライアント要求に対して一致として処理されます。
たとえば、1.0 と 2.0 のサービスを特定のバージョンで登録し、このサービスを null
バージョン付きで登録することもできます。
MEF を使用してサービスを提供および登録する
これには Visual Studio 2022 更新プログラム 2 以降が必要です。
仲介型サービスは、前の 2 つのセクションで説明したように、Visual Studio パッケージを使用する代わりに MEF 経由でエクスポートできます。 これには、考慮すべきトレードオフがあります。
トレードオフ | パッケージ提供 | MEF エクスポート |
---|---|---|
可用性 | ✅ 仲介型サービスは、VS の起動時にすぐに利用できます。 | ⚠️ プロセスで MEF が初期化されるまで、仲介型サービスの可用性に遅れが生じる可能性があります。 これは通常高速ですが、MEF キャッシュが古い場合は数秒かかることがあります。 |
クロスプラットフォームの準備 | ⚠️ Visual Studio for Windows 固有のコードを作成する必要があります。 | ✅ アセンブリ内の仲介型サービスは、Visual Studio for Windows と Visual Studio for Mac に読み込むことができます。 |
VS パッケージを使用する代わりに MEF 経由で仲介型サービスをエクスポートするには:
- 最後の 2 つのセクションに関連するコードがないことを確認します。 特に、IBrokeredServiceContainer.Proffer を呼び出すコードは使用せず、ProvideBrokeredServiceAttribute をパッケージに適用しないでください (存在する場合)。
- 仲介型サービス クラスに
IExportedBrokeredService
インターフェイスを実装します。 - コンストラクターでのメイン スレッドの依存関係や、プロパティ セッターのインポートは避けてください。 メイン スレッドの依存関係が許可されている仲介型サービスを初期化するには、
IExportedBrokeredService.InitializeAsync
メソッドを使用します。 - サービス モニカー、対象ユーザー、必要なその他の登録関連情報に関する情報を指定して、
ExportBrokeredServiceAttribute
を仲介型サービス クラスに適用します。 - MEF ではサービスの有効期間を認識し、同期的な破棄のみをサポートするため、クラスで破棄が必要な場合は、IAsyncDisposable ではなく IDisposable を実装します。
source.extension.vsixmanifest
ファイルに、仲介型サービスを含むプロジェクトが MEF アセンブリとして一覧表示されていることを確認します。
仲介型サービスでは、既定のスコープ内の他の MEF パーツを MEF パーツとしてインポート "できます"。
その場合は、必ず System.Composition.ImportAttribute ではなく System.ComponentModel.Composition.ImportAttribute を使用してください。
ExportBrokeredServiceAttribute
は System.ComponentModel.Composition.ExportAttribute から派生し、型全体で同じ MEF 名前空間を使用する必要があるためです。
仲介型サービスは、いくつかの特殊なエクスポートをインポートできるという点で独特です。
- IServiceBroker。他の仲介型サービスを取得するために使用する必要があります。
- ServiceMoniker。仲介型サービスの複数のバージョンをエクスポートし、クライアントで要求したバージョンを検出する必要がある場合に便利です。
- ServiceActivationOptions。特殊なパラメーターまたはクライアント コールバック ターゲットを指定するようクライアントに要求する場合に便利です。
- AuthorizationServiceClient。仲介型サービスをセキュリティで保護する方法に関する記事の説明に従ってセキュリティ チェックを実行する必要がある場合に役立ちます。 このオブジェクトは、仲介型サービスが破棄されるときに自動的に破棄されるため、クラスによって破棄する必要は "ありません"。
仲介型サービスでは、他の仲介型サービスを取得するために MEF の ImportAttribute を使用することは "できません"。
代わりに、従来の方法で仲介型サービスを [Import]
IServiceBroker および クエリできます。
詳細については、仲介型サービスを使用する方法に関する記事を参照してください。
例を次に示します。
using System;
using System.ComponentModel.Composition;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.ServiceHub.Framework;
using Microsoft.ServiceHub.Framework.Services;
using Microsoft.VisualStudio.Shell.ServiceBroker;
[ExportBrokeredService("Calc", "1.0")]
internal class MefBrokeredService : IExportedBrokeredService, ICalculator
{
internal static ServiceRpcDescriptor SharedDescriptor { get; } = new ServiceJsonRpcDescriptor(
new ServiceMoniker("Calc", new Version("1.0")),
clientInterface: null,
ServiceJsonRpcDescriptor.Formatters.MessagePack,
ServiceJsonRpcDescriptor.MessageDelimiters.BigEndianInt32LengthHeader,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 });
// IExportedBrokeredService
public ServiceRpcDescriptor Descriptor => SharedDescriptor;
[Import]
IServiceBroker ServiceBroker { get; set; } = null!;
[Import]
ServiceMoniker ServiceMoniker { get; set; } = null!;
[Import]
ServiceActivationOptions Options { get; set; }
// IExportedBrokeredService
public Task InitializeAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public ValueTask<int> AddAsync(int a, int b, CancellationToken cancellationToken = default)
{
return new(a + b);
}
public ValueTask<int> SubtractAsync(int a, int b, CancellationToken cancellationToken = default)
{
return new(a - b);
}
}
仲介型サービスの複数のバージョンのエクスポート
ExportBrokeredServiceAttribute
を仲介型サービスに複数回適用すると、仲介型サービスの複数のバージョンを提供できます。
IExportedBrokeredService.Descriptor
プロパティの実装では、クライアントで要求したものと一致するモニカーを持つ記述子を返す必要があります。
この例について考えてみます。MessagePack 書式設定を使用した場合にパフォーマンスを向上させるために、電卓サービスで 1.0 を UTF8 書式設定でエクスポートした後、1.1 エクスポートを追加します。
[ExportBrokeredService("Calc", "1.0")]
[ExportBrokeredService("Calc", "1.1")]
internal class MefBrokeredService : IExportedBrokeredService, ICalculator
{
internal static ServiceRpcDescriptor SharedDescriptor1_0 { get; } = new ServiceJsonRpcDescriptor(
new ServiceMoniker("Calc", new Version("1.0")),
clientInterface: null,
ServiceJsonRpcDescriptor.Formatters.UTF8,
ServiceJsonRpcDescriptor.MessageDelimiters.HttpLikeHeaders,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 });
internal static ServiceRpcDescriptor SharedDescriptor1_1 { get; } = new ServiceJsonRpcDescriptor(
new ServiceMoniker("Calc", new Version("1.1")),
clientInterface: null,
ServiceJsonRpcDescriptor.Formatters.MessagePack,
ServiceJsonRpcDescriptor.MessageDelimiters.BigEndianInt32LengthHeader,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 });
// IExportedBrokeredService
public ServiceRpcDescriptor Descriptor =>
this.ServiceMoniker.Version == SharedDescriptor1_0.Moniker.Version ? SharedDescriptor1_0 :
this.ServiceMoniker.Version == SharedDescriptor1_1.Moniker.Version ? SharedDescriptor1_1 :
throw new NotSupportedException();
[Import]
ServiceMoniker ServiceMoniker { get; set; } = null!;
}
Visual Studio 2022 Update 12 (17.12) 以降では、null
バージョン付きのサービスをエクスポートすると、null
バージョンに対する要求を含めて、バージョンに関係なく、すべてのクライアント要求と一致させることができます。
このようなサービスで、クライアントから要求されたバージョンの実装を提供していない場合は、クライアントの要求を拒否するために、Descriptor
プロパティから null
を返すことができます。
サービス要求の拒否
仲介型サービスは、InitializeAsync メソッドからスローすることによって、クライアントのアクティブ化要求を拒否できます。 スローにより、ServiceActivationFailedException がクライアントに返されます。