共用方式為


提供代理服務

代理服務包含下列元素:

上述清單中的每一個項目都會在下列各節中詳細說明。

使用本文中的所有程式碼時,強烈建議您啟用 C# 的 可空參考類型功能

服務介面

服務介面可能是標準的 .NET 介面(通常用 C# 撰寫),但應符合由 ServiceRpcDescriptor衍生型別所設定的指導方針,以確保當客戶端和服務在不同進程中運行時,介面可以透過 RPC 使用。 這些限制通常包括不允許屬性和索引器,而且大部分或所有方法都會傳回 Task 或其他異步相容的傳回型別。

ServiceJsonRpcDescriptor 是代理服務的建議衍生類型。 當客戶端和服務需要 RPC 通訊時,這個類別會利用 StreamJsonRpc 連結庫。 StreamJsonRpc 會在服務介面上套用某些限制,如 這裡所述

介面 可能會 衍生自 IDisposableSystem.IAsyncDisposable,甚至 Microsoft.VisualStudio.Threading.IAsyncDisposable,但系統不需要此專案。 產生的用戶端 Proxy 會以任一方式實作 IDisposable

簡單的計算機服務介面可能會宣告如下:

public interface ICalculator
{
    ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken);
    ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken);
}

雖然此介面上的方法實作可能不保證異步方法,但我們一律在此介面上使用異步方法簽章,因為此介面是用來產生用戶端 Proxy,而該 Proxy 可能會從遠端叫用此服務,這當然 確實 保證異步方法簽章。

介面可以宣告事件,可用來通知其用戶端服務中發生的事件。

除了事件或觀察者設計模式,代理服務在需要對用戶端進行「回呼」時,可能會定義第二個介面,此介面作為一項合約,用戶端必須在請求服務時實作並透過 ServiceActivationOptions.ClientRpcTarget 屬性提供。 這類介面應該符合與代理服務介面相同的所有設計模式和限制,但已新增版本設定的限制。

請檢閱 設計具面向未來的代理服務的最佳作法,以了解設計效能佳的 RPC 介面的建議。

在實作服務的元件中宣告這個介面很有用,如此一來,其用戶端就可以參考介面,而不需要服務公開更多實作詳細數據。 在保留您自己的擴充功能以進行服務實作的同時,將介面元件作為 NuGet 套件發佈,供其他擴充功能參考也很有幫助。

請考慮將宣告服務介面的元件設為 netstandard2.0,以確保不論服務是執行 .NET Framework、.NET Core、.NET 5 或更新版本,都可以輕鬆地從任何 .NET 進程叫用服務。

測試

自動化測試應該與您的服務一起撰寫 介面,以確認介面的 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 介面。 服務可以實作 IDisposable 或其他除了用於 RPC 之外的介面。 用戶端上產生的 Proxy 只會實作服務介面,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 發佈的所有更新依照狀態變更引入至服務的順序。

所有訂用帳戶最終都應該以呼叫 OnCompletedOnError 終止,以避免用戶端和 RPC 系統上的資源流失。 在服務處置過程中,應明確完成所有剩餘的訂閱。

深入瞭解 觀察者設計模式如何實作可觀察的數據提供者,尤其是在考慮到 RPC 的情況下

可處置的服務

您的服務類別不需要是可處置的,但是當用戶端將 Proxy 處置至您的服務或用戶端與服務之間的連線遺失時,將會處置的服務。 可處置介面會依此順序測試:System.IAsyncDisposableMicrosoft.VisualStudio.Threading.IAsyncDisposableIDisposable。 只有此清單中的第一個介面,您的服務類別會用來處置服務。

考慮處置時,請記住線程安全性。 當您的服務中的其他程式碼正在運行時,您的 Dispose 方法可能會在任何線程上被呼叫(例如,如果一個連線被中斷)。

拋出例外

擲回例外狀況時,請考慮使用特定的 ErrorCode 來擲回 LocalRpcException,以便控制用戶端在 RemoteInvocationException中收到的錯誤碼。 提供錯誤碼可以讓客戶端更好地根據錯誤的本質進行處理,比起解析例外狀況的訊息或類型更有效。

根據 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 類別如何僅創建一次,並與 Calculator 服務的每個實例共享。

在處理共享狀態時,確保執行緒安全是特別重要的,因為無法保證多個用戶端的呼叫排程不會同時進行。

如果您的共享狀態類別需要存取其他代理服務,它應該使用全域服務代理程式,而不是指派給代理服務個別實例的內容代理程式之一。 設定 ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients 旗標時,在代理服務中使用全域服務代理程式會帶來 的安全性影響。

安全性考慮

如果您的代理服務已使用 ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients 旗標註冊,那麼安全性便是一個需考量的重要因素,因為這會使參與共用 Live Share 會話的其他使用者在其他電腦上可能存取該服務。

檢閱 如何確保 Brokered Service 的安全性,並在設定 AllowTransitiveGuestClients 旗標之前採取必要的安全緩解措施。

服務名稱

代理服務必須具有可串行化的名稱,以及用戶端可以要求服務的選擇性版本。 ServiceMoniker 是用來包裝這兩項資訊的便利工具。

服務標誌類似於 CLR(通用語言執行平台)類型的組件合格完整名稱。 它必須是全域唯一的,因此應該包含您的公司名稱,並將您的擴充功能名稱作為服務名稱本身的前綴。

static readonly 欄位中定義此識別符可能在其他地方有用。

public static readonly ServiceMoniker Moniker = new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0"));

雖然大部分使用您的服務的情況可能不會直接使用名稱,但那些通過管道而不是使用代理伺服器進行通信的客戶端將需要使用此名稱。

雖然在名號上提供版本是可選的,但建議提供版本,因為這樣能為服務作者提供更多方法,以便在行為變更時保持與用戶端的相容性。

服務描述項

服務描述項會結合服務Moniker與執行 RPC 連線並建立本機或遠端 Proxy 所需的行為。 描述項負責有效地將您的 RPC 介面轉換成有線通訊協定。 此服務描述項是 ServiceRpcDescriptor衍生類型的實例。 描述項必須提供給將使用 Proxy 存取此服務的所有用戶端。 提供服務也需要這個描述符。

Visual Studio 會定義一種這類衍生類型,並建議將其用於所有服務:ServiceJsonRpcDescriptor。 此描述子會利用 StreamJsonRpc 進行 RPC 連線,並為本機服務創建一個高效能的本機代理,以模擬某些遠端行為特性,例如,將服務擲出的例外狀況包裝在 RemoteInvocationException中。

ServiceJsonRpcDescriptor 支援設置 JsonRpc 類別,以便對 JSON-RPC 協定進行 JSON 或 MessagePack 編碼。 我們建議使用 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 連線只是 多任務數據流上的一個通道,該通道會與 JSON-RPC 連線共用,透過 JSON-RPC有效傳輸大型二進位數據。

ExceptionProcessing.ISerializable 策略會導致從您的服務擲回的例外狀況被串行化,並作為用戶端上擲回的 Exception.InnerExceptionRemoteInvocationException 保留。 如果沒有此設定,用戶端上就提供較不詳細的例外狀況資訊。

提示:將您的描述符公開為 ServiceRpcDescriptor,而不是您作為實作細節使用的任何衍生類型。 這可讓您更彈性地稍後變更實作詳細數據,而不需要 API 中斷性變更。

在描述元的 xml 檔批註中包含服務介面的參考,讓使用者更容易取用您的服務。 此外,如果適用,也參考服務接受的介面做為用戶端 RPC 目標。

某些更進階的服務也可能接受或要求來自符合某些介面之用戶端的 RPC 目標物件。 在這種情況下,請使用 ServiceJsonRpcDescriptor 建構函式搭配 Type clientInterface 參數來指定用戶端應提供 實例的介面。

版本化描述符

一段時間后,您可能會想要遞增服務的版本。 在這種情況下,您應該為每個想要支援的版本定義描述元,並針對每個版本使用唯一的版本特定 ServiceMoniker。 同時支援多個版本可能有利於回溯相容性,而且通常只能使用一個 RPC 介面來完成。

Visual Studio 會遵循此模式及其 VisualStudioServices 類別,方法是將原始 ServiceRpcDescriptor 定義為巢狀類別底下的 virtual 屬性,代表新增該代理服務的第一個版本。 當我們需要變更有線通訊協定或新增/變更服務的功能時,Visual Studio 會在更新版本的巢狀類別中宣告 override 屬性,以傳回新的 ServiceRpcDescriptor

針對 Visual Studio 擴充功能所定義和提供的服務,可能只需在原始專案旁邊宣告另一個描述子屬性。 例如,假設您的 1.0 服務使用 UTF8 (JSON) 格式器,而且您意識到切換至 MessagePack 會提供顯著的效能優勢。 當變更格式器造成通訊協定中斷時,這需要增加仲介服務的版本號碼和第二個描述符。 這兩個描述元在一起看起來可能像這樣:

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 });

雖然我們宣告了兩個描述元(稍後我們必須教授並註冊兩個服務),但我們可以只使用一個服務介面和實作來執行這項操作,讓支援多個服務版本的額外負荷相當低。

提供服務

當請求進入時,必須建立您的中介服務,這會透過一個稱為「提供服務」的步驟來安排。

服務工廠

使用 GlobalProvider.GetServiceAsync 來要求 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()));

代理服務通常會在每個用戶端實例化一次。 這與 其他 VS (Visual Studio) 服務不同,通常會在所有客戶端之間具現化一次並共用。 每個客戶端創建一個服務實例,可以為每個服務及其連線提供更佳的安全性,並可保留關於客戶端運作權限等級和其偏好CultureInfo等的專屬狀態。正如我們將看到的,這也使得更具吸引力的服務得以實現,這些服務能夠接受針對特定請求的自變數。

重要

偏離此指導方針並傳回共用服務實例的服務處理站,而不是每個用戶端的新實例,應該 永遠不會 其服務實作 IDisposable,因為第一個處置其 Proxy 的用戶端會導致處置共用服務實例,然後再使用其他用戶端。

CalculatorService 建構函式需要共享狀態物件和 IServiceBroker的更進階案例中,我們可能會提供如下的處理站:

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

state 局部變數 服務處理站外部,因此只會建立一次,並跨所有具現化服務共用。

更進階的是,如果服務需要存取 ServiceActivationOptions(例如,若要在用戶端 RPC 目標物件上叫用方法),也可以傳入:

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

在此情況下,服務建構函式可能是這樣的,假設 ServiceJsonRpcDescriptor 是以 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,否則請避免使用 AuthorizingBrokeredServiceFactory 委派搭配 IBrokeredServiceContainer.Proffer 方法。 此 IAuthorizationService必須使用您的仲介服務類別處置,以避免記憶體洩漏。

支援多個版本的服務

當您在 ServiceMoniker上更新版本時,您必須提供您希望用來回應客戶端請求的每個代理服務版本。 這是藉由呼叫 IBrokeredServiceContainer.Proffer 方法,並呼叫您仍然支援的每個 ServiceRpcDescriptor

使用 null 版本來提供您的服務,將作為「通配符」,當用戶端的需求沒有與已註冊服務的精確版本相符時,它將予以匹配。 例如,您可以使用特定版本來提供您的 1.0 和 1.1 服務,以及向 null 版本註冊您的服務。 在這種情況下,使用 1.0 或 1.1 要求服務的用戶端會叫用您針對這些確切版本所提供的服務工廠,而要求 8.0 版的用戶端會導致叫用您無版本提供的服務工廠。 因為用戶端要求的版本會提供給服務處理站,因此處理站接著可能會決定如何為此特定客戶端設定服務,或是否傳回 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 Package,以執行提供程式碼。 這可讓 Visual Studio 快速啟動,而不需要事先載入所有延伸模組,但能夠在其代理服務的用戶端要求時載入所需的擴充功能。

您可以將 ProvideBrokeredServiceAttribute 套用至 AsyncPackage衍生類別來完成註冊。 這是唯一可以設定 ServiceAudience 的地方。

[ProvideBrokeredService("YourCompany.Extension.Calculator", "1.0", Audience = ServiceAudience.Local)]

默認 AudienceServiceAudience.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 Update 2 或更新版本。

代理服務可以透過MEF匯出,而不是使用Visual Studio套件,如前兩節所述。 這有取捨需要考慮:

權衡取捨 套件提供 MEF 匯出
可用性 ✅ Brokered 服務可在 VS 啟動時立即使用。 ⚠因為在程式中 MEF 尚未初始化,中介服務的可用性可能會延遲。 這通常是快速的,但MEF快取過時時可能需要幾秒鐘的時間。
跨平臺整備程度 ⚠必須撰寫特定於 Visual Studio for Windows 的代碼。 ✅ 元件中的代理服務可以載入於 Visual Studio for Windows 和 Visual Studio for Mac。

若要透過MEF匯出代理服務,而不是使用 VS 套件:

  1. 確認您沒有與最後兩個區段相關的程式碼。 特別是,您應該沒有呼叫 IBrokeredServiceContainer.Proffer 的程式代碼,而且不應該將 ProvideBrokeredServiceAttribute 套用至您的套件(如果有的話)。
  2. 在您的代理服務類別上實作 IExportedBrokeredService 介面。
  3. 請避免在建構函式中引入任何主執行緒的相依性,或導入屬性 setter。 使用 IExportedBrokeredService.InitializeAsync 方法來初始化代理服務,其中允許主要線程相依性。
  4. ExportBrokeredServiceAttribute 套用至您的代理服務類別,並指定您的服務代號、受眾及其他註冊所需的相關資訊。
  5. 如果您的類別需要處置,請實作 IDisposable,而不是 IAsyncDisposable,因為MEF擁有服務的存留期,且僅支援同步處置。
  6. 請確保您的 source.extension.vsixmanifest 檔案將包含您代理服務的專案列為 MEF 組件。

作為MEF部分,您的代理服務 可能會 在預設範圍中匯入任何其他MEF元件。 這樣做時,請務必使用 System.ComponentModel.Composition.ImportAttribute,而不是 System.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 屬性實作應該傳回一個描述符,其中包含一個與客戶端請求相匹配的別名。

請考慮此範例,其中計算服務使用UTF8格式導出1.0,然後稍後新增1.1導出,以享受使用MessagePack格式的效能提升。

[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 擲回用戶端。