次の方法で共有


ブローカー サービスを提供する

ブローカー サービスは、次の要素で構成されます。

前の一覧の各項目について、以降のセクションで詳しく説明します。

この記事のすべてのコードで、C# の null 許容参照型機能をアクティブ化することを強くお勧めします。

サービス インターフェイス

サービス インターフェイスは標準の .NET インターフェイス (多くの場合 C# で記述) である場合がありますが、クライアントとサービスが異なるプロセスで実行されるときに RPC 経由でインターフェイスを使用できるようにするために、サービスが使用する ServiceRpcDescriptor派生型によって設定されたガイドラインに従う必要があります。 これらの制限には、通常、プロパティとインデクサーは許可されず、ほとんどのメソッドまたはすべてのメソッドは、Task または別の非同期互換性のある戻り値の型を返します。

ServiceJsonRpcDescriptor は、ブローカー サービスに推奨される派生型です。 このクラスは、クライアントとサービスが通信するために RPC を必要とする場合に、StreamJsonRpc ライブラリを利用します。 StreamJsonRpc は、ここで説明する サービス インターフェイスに特定の制限を適用します。

インターフェイス は、IDisposableSystem.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 フィールドを追加できます。

イベント

インターフェイスで宣言されたイベントは、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 インターフェイスを実装する必要があります。 サービスは、RPC に使用されるインターフェイス以外の IDisposable またはその他のインターフェイスを実装できます。 クライアントで生成されたプロキシは、サービス インターフェイス、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.IAsyncDisposableMicrosoft.VisualStudio.Threading.IAsyncDisposableIDisposable。 サービス クラスが実装するこの一覧の最初のインターフェイスのみが、サービスの破棄に使用されます。

廃棄を検討する際は、スレッドの安全性に注意してください。 Dispose メソッドは、サービス内の他のコードの実行中 (接続が切断されている場合など) に、任意のスレッドで呼び出される可能性があります。

例外のスロー

例外をスローする場合は、RemoteInvocationExceptionでクライアントが受け取ったエラー コードを制御するために、特定の ErrorCodeLocalRpcException をスローすることを検討してください。 クライアントにエラー コードを提供すると、例外メッセージや型を解析するよりも、エラーの性質に基づいてクライアントを分岐させることができます。

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 Package によってインスタンス化され、サービスのコンストラクターに引数として渡される個別のクラスで定義する必要があります。

上記で定義した 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 つのサービス インターフェイスと実装だけでこれを行い、複数のサービス バージョンをサポートするためのオーバーヘッドを非常に低く保ちます。

サービスの提供

仲介型サービスは、要求が到着したときに作成する必要があります。これは、サービスの提供と呼ばれる手順を介して配置されます。

サービス ファクトリ

GlobalProviderGetServiceAsync を使用して SVsBrokeredServiceContainerを要求します。 次に、そのコンテナーの IBrokeredServiceContainer.Proffer を呼び出して、サービスを提供します。

次の例では、前に宣言した CalculatorService フィールドを使用してサービスを提供します。これは、ServiceRpcDescriptorのインスタンスに設定されています。 これを、BrokeredServiceFactory デリゲートであるサービス ファクトリに渡します。

IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
container.Proffer(
    CalculatorService,
    (moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService()));

通常、ブローカー サービスはクライアントごとに 1 回インスタンス化されます。 これは、他 VS (Visual Studio) サービスから逸脱しています。これは通常、1 回インスタンス化され、すべてのクライアント間で共有されます。 クライアントごとにサービスのインスタンスを 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 回だけ作成され、インスタンス化されたすべてのサービス間で共有されます。

さらに高度です。サービスが、渡される可能性のある ServiceActivationOptions (クライアント RPC ターゲット オブジェクトでメソッドを呼び出すなど) へのアクセスを必要とする場合は、次のようになります。

var state = new CalculatorService.State();
container.Proffer(CalculatorService, (moniker, options, serviceBroker, cancellationToken) =>
    new ValueTask<object?>(new CalculatorService(state, serviceBroker, options)));

この場合、ServiceJsonRpcDescriptor がコンストラクター引数の 1 つとして typeof(IClientCallbackInterface) で作成されたと仮定すると、サービス コンストラクターは次のようになります。

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のバージョンをインクリメントするときは、クライアント要求に応答するブローカー サービスの各バージョンを指定する必要があります。 これを行うには、引き続きサポートする各 ServiceRpcDescriptorIBrokeredServiceContainer.Proffer メソッドを呼び出します。

null バージョンを使用したサービスの提供は、登録済みのサービスと正確に一致したバージョンが存在しないクライアント要求にも一致する "キャッチ オール" として機能します。 たとえば、1.0 と 1.1 のサービスを特定のバージョンで提供し、サービスを null バージョンに登録することもできます。 このような場合、1.0 または 1.1 でサービスを要求するクライアントは、これらの正確なバージョンに対して指定したサービス ファクトリを呼び出します。一方、バージョン 8.0 を要求するクライアントは、null バージョンのプロファード サービス ファクトリが呼び出されます。 クライアントが要求したバージョンはサービス ファクトリに提供されるため、ファクトリは、この特定のクライアントのサービスを構成する方法、またはサポートされていないバージョンを示すために null を返すかどうかを決定できます。

null バージョン "のみ" があるサービスに対するクライアント要求は、null バージョンで登録され、提供されたサービスに一致します。

多くのバージョンのサービスを公開しており、そのうちのいくつかは下位互換性があり、サービス実装を共有する可能性がある場合を考えてみましょう。 catch-all オプションを使用すると、次のように各バージョンを繰り返し提供する必要がなくなります。

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)]

既定の AudienceServiceAudience.Processであり、ブローカー サービスは同じプロセス内の他のコードにのみ公開されます。 ServiceAudience.Local設定すると、ブローカー サービスを同じ Visual Studio セッションに属する他のプロセスに公開することを選択できます。

ブローカー サービス が Live Share ゲストに公開 必要がある場合は、AudienceServiceAudience.LiveShareGuest を含め、ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients プロパティを trueに設定する必要があります。 これらのフラグを設定すると、 重大なセキュリティの脆弱性が発生する可能性があり、「ブローカー サービスの をセキュリティで保護する方法」のガイダンスに最初に従わなければ実行しないでください。

ServiceMonikerのバージョンをインクリメントするときは、クライアント要求に応答するブローカー サービスの各バージョンを登録する必要があります。 ブローカー サービスの最新バージョンよりも多くのバージョンをサポートすることで、以前のブローカー サービス バージョンのクライアントの下位互換性を維持するのに役立ちます。これは、セッションを共有している Visual Studio の各バージョンが異なる可能性がある Live Share シナリオを検討する場合に特に便利です。

null バージョンを使用したサービスの登録は、登録済みのサービスと正確に一致したバージョンが存在しないクライアント要求にも一致する "キャッチ オール" として機能します。 たとえば、1.0 と 2.0 のサービスを特定のバージョンに登録し、サービスを null バージョンに登録することもできます。

MEF を使用してサービスの提供と登録を行う

Visual Studio 2022 Update 2 以降が必要です。

前の 2 つのセクションで説明したように、Visual Studio パッケージを使用する代わりに、ブローカー サービスを MEF 経由でエクスポートできます。 これには考慮すべきトレードオフがあります。

トレードオフ パッケージ提供 MEF エクスポート
可用性 ✅ ブローカー サービスは、VS の起動時にすぐに使用できます。 ️ ⚠ブローカー サービスは、プロセスで MEF が初期化されるまで可用性が遅れる可能性があります。 通常、これは高速ですが、MEF キャッシュが古い場合、数秒かかる場合があります。
クロスプラットフォーム対応性 ️ ⚠Visual Studio for Windows 固有のコードを作成する必要があります。 ✅ アセンブリ内のブローカー サービスは、Visual Studio for Windows および Visual Studio for Mac に読み込むことができます。

VS パッケージを使用する代わりに MEF 経由でブローカー サービスをエクスポートするには:

  1. 最後の 2 つのセクションに関連するコードがないことを確認します。 特に、IBrokeredServiceContainer.Proffer を呼び出すコードはなく、パッケージに ProvideBrokeredServiceAttribute を適用しないでください (存在する場合)。
  2. ブローカー サービス クラスに IExportedBrokeredService インターフェイスを実装します。
  3. コンストラクター内のメイン スレッドの依存関係や、プロパティ セッターのインポートは避けてください。 メイン スレッドの依存関係が許可されているブローカー サービスを初期化するには、IExportedBrokeredService.InitializeAsync メソッドを使用します。
  4. ExportBrokeredServiceAttribute をブローカー サービス クラスに適用し、サービス モニカー、対象ユーザー、およびその他の登録関連情報に関する情報を指定します。
  5. クラスで破棄が必要な場合は、MEF がサービスの有効期間を所有し、同期破棄のみをサポートするため、IAsyncDisposable ではなく IDisposable を実装します。
  6. source.extension.vsixmanifest ファイルに、ブローカー サービスを含むプロジェクトが MEF アセンブリとして一覧表示されていることを確認します。

MEF パーツとして、ブローカー サービス は、既定のスコープ内の他の MEF パーツを インポートすることができます。 その場合は、必ず System.Composition.ImportAttributeではなく System.ComponentModel.Composition.ImportAttribute を使用してください。 これは、ExportBrokeredServiceAttributeSystem.ComponentModel.Composition.ExportAttribute から派生し、型全体で同じ MEF 名前空間を使用する必要があるためです。

ブローカー サービスは、いくつかの特殊なエクスポートをインポートできることで一意です。

  • IServiceBrokerは、他のブローカーサービスを取得するために利用する必要があります。
  • ServiceMoniker。複数のバージョンのブローカー サービスをエクスポートし、クライアントが要求したバージョンを検出する必要がある場合に便利です。
  • ServiceActivationOptions。これは、クライアントに特別なパラメーターまたはクライアント コールバック ターゲットの指定を要求する場合に役立ちます。
  • AuthorizationServiceClient。ブローカー サービス をセキュリティで保護する方法に関する説明されているように、セキュリティ チェックを実行する必要がある場合に役立ちます。 このオブジェクトは、仲介型サービスが破棄されるときに自動的に破棄されるため、クラスによって破棄される必要は "ありません"。

ブローカー サービス は、MEF の ImportAttribute を使用して他のブローカー サービスを取得 することはできません。 代わりに、IServiceBroker[Import] を行って、従来の方法で仲介型サービスのクエリを実行できます。 詳細については、「ブローカー サービスを使用する方法」を参照してください。

サンプルを次に示します。

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 プロパティの実装では、クライアントが要求したものと一致するモニカーを持つ記述子を返す必要があります。

この例では、電卓サービスが UTF8 形式で 1.0 をエクスポートした後、MessagePack 書式設定のパフォーマンスを向上させるために 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 がクライアントにスローされます。