共用方式為


取用代理服務

本文件旨在說明與取得、一般使用及處置任何代理服務相關的所有程式碼、模式和警告。 若要了解如何在取得之後使用特定代理服務,請查閱該代理服務的特定文件。

強烈建議使用本文件中的所有程式碼,並啟用 C# 的可為 Null 參考類型功能。

擷取 IServiceBroker

若要取得代理服務,您必須先有 IServiceBroker 的執行個體。 當您的程式碼是在 MEF (Managed Extensibility Framework) 或 VSPackage 的內容中執行時,您通常會需要全域 Service Broker。

代理服務本身應該使用IServiceBroker叫用其服務工廠時所指派的

全域 Service Broker

Visual Studio 提供兩種方式來取得全域 Service Broker。

使用GlobalProviderGetServiceAsync來要求SVsBrokeredServiceContainer

IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
IServiceBroker serviceBroker = container.GetFullAccessServiceBroker();

從 Visual Studio 2022 開始,在 MEF 啟動擴充功能中執行的程式碼可以匯入全域 Service Broker:

[Import(typeof(SVsFullAccessServiceBroker))]
IServiceBroker ServiceBroker { get; set; }

請注意必要 Import 屬性的 typeof 引數。

全域 IServiceBroker 的每個要求都會產生物件的新執行個體,做為全域代理服務容器的檢視。 Service Broker 的這個唯一執行個體可讓您的用戶端接收該用戶端所使用的唯一 AvailabilityChanged 事件。 建議您使用上述任一方法讓擴充功能中的每個用戶端/類別取得各自的 Service Broker,而不是取得一個執行個體,並在整個擴充功能中共用它。 此模式也鼓勵使用安全編碼模式,亦即代理服務不應使用全域 Service Broker。

重要

IServiceBroker 的執行個體通常不會實作 IDisposable,但是當 AvailabilityChanged 處理常式存在時,就無法收集這些物件。 請務必平衡事件處理常式的新增/移除,尤其是在程式碼可能會在程序的存留期間捨棄 Service Broker 時。

內容特定 Service Broker

使用適當的 Service Broker 是代理服務安全性模型的重要需求,尤其是在 Live Share 工作階段的內容中。

代理服務會以自己的 IServiceBroker 方式啟動,而且應該針對任何自己的代理服務需求使用此執行個體,包括向 Proffer 提供的服務。 這類程式碼會提供 BrokeredServiceFactory,接收具現化代理服務要使用的 Service Broker。

擷取代理服務 Proxy

擷取代理服務通常是使用 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 區塊時處置 Proxy。

重要

即使服務介面不是衍生自 IDisposable,也必須處置擷取的每個 Proxy。 處置很重要,因為 Proxy 通常會有 I/O 資源,可阻止其進行垃圾收集。 處置會終止 I/O,允許 Proxy 進行垃圾收集。 使用條件式轉換來IDisposable處置,並備妥轉換,以避免null未實際實作的一或多個 ProxyIDisposable 發生例外狀況。

請務必安裝最新的 Microsoft.ServiceHub.Analyzers NuGet 套件,並保留啟用 ISBxxxx 分析器規則,以協助防止這類洩漏。

Proxy 的處置會導致處置該用戶端專用的代理服務。

如果您的程式碼需要代理服務,而且無法在服務無法使用時完成其工作,則如果程式碼擁有使用者體驗 (而不是擲回例外狀況),您可能會想要向使用者顯示錯誤對話方塊。

用戶端 RPC 目標

某些代理服務接受或需要客戶端 RPC (遠端程序呼叫) 目標來進行「回呼」。此類選項或要求應包含在該特定代理服務的文件中。 針對 Visual Studio 代理服務,這項資訊應該包含在描述項的 IntelliSense 文件中。

在這種情況下,用戶端可以使用與以下類似的 ServiceActivationOptions.ClientRpcTarget 來提供:

IMyService? myService = await broker.GetProxyAsync<IMyService>(
    serviceDescriptor,
    new ServiceActivationOptions
    {
        ClientRpcTarget = new MyCallbackObject(),
    },
    cancellationToken);

叫用用戶端 Proxy

要求代理服務的結果是 Proxy 所實作的服務介面執行個體。 此 Proxy 會在每個方向轉送呼叫和事件,其行為與直接呼叫服務時所預期的行為有一些重要差異。

觀察者模式

如果服務合約採用IObserver<T> 類型的參數,您可以在如何實作觀察者中深入了解如何建構這類類型。

ActionBlock<TInput>可以調整為IObserver<T>使用 AsObserver 擴充方法實作。 來自回應架構的 System.Reactive.Observer 類別是自行實作介面的另一個替代方案。

從 Proxy 擲回的例外狀況

  • 預期 RemoteInvocationException 會針對從代理服務擲回的任何例外狀況擲回。 您可以在 InnerException 中找到原始例外狀況。 這是遠端託管服務的自然行為,因為它是來自 JsonRpc 的行為。 當服務位於本機時,本機 Proxy 會以相同方式包裝所有例外狀況,讓用戶端程式碼只能有一個適用於本機和遠端服務的例外狀況路徑。
  • 當遠端服務連線中斷或託管服務的程序當機時,預期 ConnectionLostException 會從任何呼叫擲回。 這主要是當服務可以從遠端取得時所關心的問題。

Proxy 的快取

在啟用代理服務和相關聯的 Proxy 時,會產生一些費用,尤其是當服務來自遠端程序時。 當經常使用保證將 Proxy 快取到類別中的許多呼叫的代理服務時,Proxy 可以儲存在該類別的欄位中。 包含類別應該是可處置的,並處置其 Dispose 方法內的 Proxy。 請考慮此範例:

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

上述程式碼大致正確,但不會考慮 DisposeSayHiAsync 之間的競爭狀況。 程式碼也不會考慮 AvailabilityChanged 應該會導致處理先前取得的 Proxy,以及下次需要重新取得 Proxy 時重新取得 Proxy 的事件。

ServiceBrokerClient 類別的設計訴求是處理這些競爭和失效條件,以協助讓您自己的程式碼保持簡單。 請考慮使用這個協助程式類別快取 Proxy 的這個更新範例:

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 Proxy 的每個租用。 處置和使用 Proxy 之間的競爭條件是由 ServiceBrokerClient 物件處理,該物件會在其本身處置時或釋放該 Proxy 的最後一次租用時處置每個快取的 Proxy,以最晚發生者為準。

關於 ServiceBrokerClient 的重要注意事項

選擇 IServiceBroker 或 ServiceBrokerClient

兩者均為易記名稱,預設值可能是 IServiceBroker

類別 IServiceBroker ServiceBrokerClient
易記名稱 Yes Yes
需要處置 No Yes
管理 Proxy 的存留期 否。 擁有者在使用 Proxy 時必須處置 Proxy。 是,只要它們有效,它們就會保持運作並重複使用。
適用於無狀態服務 Yes Yes
適用於具狀態服務 No
當事件處理常式新增至 Proxy 時適用 No
當舊 Proxy 失效時要通知的事件 AvailabilityChanged Invalidated

ServiceBrokerClient 提供方便的方法,可讓您快速且頻繁地重複使用 Proxy,而您並不在意基礎服務是否在最上層作業之間變更。 但是,如果您確實關心這些事項,而且想要自行管理 Proxy 的存留期,或您需要事件處理程式 (這表示您需要管理 Proxy 的存留期),則應該使用 IServiceBroker

服務中斷的復原能力

代理服務可能會發生幾種服務中斷:

代理服務啟用失敗

當可用的服務可以滿足代理服務要求,但服務工廠擲回未處理的例外狀況時,會將 ServiceActivationFailedException 擲回給用戶端, 以便他們了解並回報失敗給使用者。

當代理服務要求無法與任何可用的服務相符時,null 會傳回給用戶端。 在這種情況下,AvailabilityChanged 將會在稍後使用該服務時引發。

服務要求可能會因為服務不存在而遭到拒絕,但因為所提供的版本低於所要求的版本。 您的後援方案可能包括使用用戶端知道的較低版本重試服務要求,且能夠與其互動。

如果/當所有失敗版本檢查的延遲變得明顯時,用戶端可以要求 VisualStudioServices.VS2019_4Services.RemoteBrokeredServiceManifest,以獲得從遠端來源取得哪些服務和版本的完整概念。

處理已中斷的連線

成功取得的代理服務 Proxy 可能會因為連線中斷或託管它的程序當機而失敗。 在這類中斷之後,在該 Proxy 上進行的任何呼叫都會導致 ConnectionLostException 擲回。

代理服務用戶端可以藉由處理 Disconnected 事件,主動偵測並回應這類連線中斷。 若要觸達此事件,必須將 Proxy 轉換成 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 事件,來重新查詢其先前查詢的代理服務時收到通知。 在要求代理服務之前,應該先新增此事件的處理常式,以確保在提出服務要求後不久引發的事件,不會因為競爭狀況而遺失。

只有在某個非同步方法執行期間才要求代理服務時,不建議處理此事件。 此事件與儲存 Proxy 一段時間的用戶端最相關,因此它們需要補償服務變更,並且能夠重新整理其 Proxy。

此事件可以在任何執行緒上引發,可能同時引發至使用事件所描述之服務的程式碼。

數個狀態變更可能會導致引發此事件,包括:

  • 正在開啟或關閉的解決方案或資料夾。
  • 開始的 Live Share 工作階段。
  • 剛探索到的動態註冊代理服務。

無論該要求是否已完成,受影響的代理服務只會導致將此事件引發至先前要求該服務的用戶端。

在該服務的每個要求之後,每個服務最多只會引發一次事件。 例如,如果用戶端要求 A 服務和 B 服務發生可用性變更,就不會將任何事件引發至該用戶端。 稍後,當 A 服務遇到可用性變更時,用戶端將會收到事件。 如果用戶端沒有重新要求 A 服務,A 的後續可用性變更將不會對該用戶端產生任何進一步通知。 一旦用戶端再次要求A,它就有資格接收有關該服務的下一個通知。

當服務變成可用、不再可用或遇到實作變更時,會引發此事件,該變更會要求所有先前的服務用戶端重新查詢服務。

ServiceBrokerClient 會自動處理有關快取 Proxy 的可用性變更事件,方法是在傳回任何租用時處置舊的 Proxy,並在其擁有者要求時,要求新的服務執行個體。 當服務無狀態,且不需要程式碼將事件處理常式附加至 Proxy 時,這個類別可以大幅簡化程式碼。

擷取代理服務管道

雖然透過 Proxy 存取代理服務是最常見的且方便的技術,但在進階案例中,最好或有必要要求該服務的管道,讓用戶端可以直接控制 RPC 或直接與任何其他資料類型進行通訊。

透過 GetPipeAsync 方法可取得代理服務的管道。 這個方法採用 ServiceMoniker, 而不是 ServiceRpcDescriptor,因為不需要描述項所提供的 RPC 行為。 當您有描述項時,您可以透過 ServiceRpcDescriptor.Moniker 屬性從中取得 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);
}