提供代理服務
代理服務包含下列元素:
- 宣告服務功能的介面,並做為服務與其用戶端之間的合約。
- 該介面的實作。
- 將名稱和版本指派給服務的服務 Moniker。
- 描述項,結合服務 Moniker 與在必要時處理 RPC (遠端程序呼叫) 的行為。
- 提供服務工廠,並使用 VS 套件註冊您的代理服務,或同時使用 MEF (Managed Extensibility Framework)。
以下各節詳細描述了上述清單中的每個項目。
強烈建議使用本文中的所有程式碼,並啟用 C# 的可為 Null 參考類型功能。
服務介面
服務介面可能是標準 .NET 介面 (通常是以 C# 撰寫),但應該符合 ServiceRpcDescriptor 衍生類型所設定的指導方針,您的服務將用來確保當用戶端和服務在不同的程序中執行時,可以透過 RPC 使用介面。
這些限制通常包括不允許屬性和索引器,而且大部分或所有方法都會傳回 Task
或其他非同步相容的傳回類型。
ServiceJsonRpcDescriptor 是代理服務的建議衍生類型。 當用戶端和服務需要 RPC 進行通訊時,這個類別會利用 StreamJsonRpc 程式庫。 StreamJsonRpc 會對服務介面套用某些限制,如這裡所述。
介面可能衍生自 IDisposable、 System.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 張貼的所有更新都會保留狀態變更引入服務的順序。
所有訂用帳戶最終都應該以呼叫 OnCompleted 終止,或 OnError 以避免用戶端和 RPC 系統上的資源流失。 這包括應該明確完成所有剩餘訂用帳戶的服務處置。
深入了解觀察者設計模式、如何實作可觀察的資料提供者,尤其是考慮 RPC。
可處置的服務
您的服務類別不需要是可處置的,但是當用戶端將 Proxy 處置至您的服務或用戶端與服務之間的連線遺失時,這些服務將被處置。 可處置介面會依下列順序測試:System.IAsyncDisposable、Microsoft.VisualStudio.Threading.IAsyncDisposable、IDisposable。 只有您服務類別實作的該清單中的第一個介面會用於處置該服務。
考慮處置時,請切記執行緒安全。 當服務中的其他程式碼正在執行時,可能會在任何執行緒上呼叫您的 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
服務的每個執行個體共用。
處理共用狀態時,特別重要的是安全執行緒,因為無法針對多個用戶端排程其呼叫進行假設,因此永遠不會同時進行。
如果您的共用狀態類別需要存取其他代理服務,它應該使用全域 Service Broker,而不是指派給代理服務個別執行個體的內容代理程式之一。 在代理服務內使用全域 Service Broker,會在設定 ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients 旗標時具有安全性影響。
安全性考量
如果已向 ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients 旗標註冊,則安全性是代理服務的考量,這會讓其他使用者在其他參與共用 Live Share 工作階段的電腦上存取。
檢閱如何保護代理服務,並在設定 AllowTransitiveGuestClients 旗標之前採取必要的安全性防護措施。
服務 Moniker
代理服務必須具有可序列化的名稱和選擇性的版本,用戶端才能要求服務。 ServiceMoniker 是這兩個資訊片段的便利包裝函式。
服務 Moniker 類似於 CLR (Common Language Runtime) 類型的組件限定完整名稱。 它必須是全域唯一的,因此應該包含您的公司名稱,或許您的擴充功能名稱是服務名稱本身的前置詞。
在 static readonly
欄位中定義此 Moniker 以用於其他地方,這可能是很有用做法:
public static readonly ServiceMoniker Moniker = new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0"));
雖然大部分的服務使用方式可能不會直接使用 Moniker,但透過管道進行通訊的用戶端 (而不是 Proxy) 將需要 Moniker。
雖然版本在Moniker上是選擇性的,但建議提供版本,因為它為服務作者提供更多選項,以便跨行為變更維護與用戶端的相容性。
服務描述項
服務描述項會結合服務 Moniker 與執行 RPC 連線並建立本機或遠端 Proxy 所需的行為。 描述項負責有效地將您的 RPC 介面轉換成有線通訊協定。 此服務描述項是 ServiceRpcDescriptor 衍生類型的執行個體。 描述項必須提供給將使用 Proxy 存取此服務的所有用戶端。 提供服務也需要這個描述項。
Visual Studio 會定義一種這類衍生類型,並建議將其用於所有服務:ServiceJsonRpcDescriptor。 此描述項會將 StreamJsonRpc 用於其 RPC 連線,並為本機服務建立高效能的本機 Proxy,以模擬服務擲回的一些遠端行為,例如包裝 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 系統的 Interop |
藉由將 MultiplexingStream.Options
物件指定為最終參數,用戶端與服務之間共用的 RPC 連線只是 MultiplexingStream 上的一個通道,該通道會與 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
欄位。
如果服務工廠是共用方法,根據 Moniker 建立多個服務或不同版本的服務,BrokeredServiceFactory 委派會採用 ServiceMoniker 做為參數。 此 Moniker 來自用戶端,並包含其預期的服務版本。 藉由將此 Moniker 轉送至服務建構函式,服務可能會模擬特定服務版本的怪異行為,以符合用戶端可能預期的行為。
除非您將使用代理服務類別內的 IAuthorizationService,否則請避免搭配 IBrokeredServiceContainer.Proffer 方法使用 AuthorizingBrokeredServiceFactory 委派。 這IAuthorizationService必須使用您的代理服務類別處置,以避免記憶體流失。
支援您的服務的多個版本
當您在 ServiceMoniker 上遞增版本時,您必須提供您想要回應用戶端要求的每個代理服務版本。 這是藉由呼叫 IBrokeredServiceContainer.Proffer 方法,以及您仍然支援的每個 ServiceRpcDescriptor 方法來完成。
提供 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 快速啟動,而不需要事先載入所有擴充功能,但能夠在其代理服務的用戶端要求時載入所需的擴充功能。
註冊可以藉由將 ProvideBrokeredServiceAttribute 套用至 AsyncPackage 衍生類別來完成。 這是唯一可以設定 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 Update 2 或更新版本。
代理服務可以透過 MEF (而不是使用 Visual Studio 套件) 匯出,如前兩節所述。 這有一些權衡取捨需要考慮:
權衡取捨 | 套件提供 | MEF 匯出 |
---|---|---|
可用性 | ✅ 代理服務可在 VS 啟動時立即使用。 | ⚠️ 代理服務可能會在可用性上延遲,直到 MEF 已在程序中初始化為止。 這通常是很快,但在 MEF 快取過時的情況下,可能需要幾秒鐘的時間。 |
跨平台整備程度 | ⚠️ 必須撰寫適用於 Windows 的 Visual Studio 特定程式碼。 | ✅ 組件中的代理服務可能會載入 Visual Studio for Windows 和 Visual Studio for Mac。 |
若要透過 MEF (而不是使用 VS 套件) 匯出代理服務:
- 確認您沒有與最後兩個區段相關的程式碼。 尤其是,您應該沒有任何呼叫 IBrokeredServiceContainer.Proffer 的程式碼,而且不應該將 ProvideBrokeredServiceAttribute 套用至您的套件 (如果有的話)。
- 在您的代理服務類別上實作
IExportedBrokeredService
介面。 - 請避免建構函式中的任何主要執行緒相依性,或匯入屬性 Setter。 使用
IExportedBrokeredService.InitializeAsync
方法來初始化代理服務,其中允許主要執行緒相依性。 - 將
ExportBrokeredServiceAttribute
套用至您的代理服務類別,並指定服務 Moniker、物件和任何其他註冊相關資訊的相關資訊。 - 如果您的類別需要處置,請實作 IDisposable,而不是 IAsyncDisposable ,因為 MEF 擁有服務的存留期,而且只支援同步處置。
- 請確保您的
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
實作應該會傳回描述項,其 Moniker 符合所要求用戶端的 Moniker。
請考慮此範例,其中計算機服務使用 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 擲回給用戶端。