Each of the items in the preceding list are described in detail in the following sections.
With all code in this article, activating C#'s nullable reference types feature is highly recommended.
The service interface
The service interface may be a standard .NET interface (often written in C#), but should conform to the guidelines set by the ServiceRpcDescriptor-derived type that your service will use to ensure the interface can be used over RPC when the client and service run in different processes.
These restrictions typically include that properties and indexers are not allowed, and most or all methods return Task or another async-compatible return type.
The ServiceJsonRpcDescriptor is the recommended derived type for brokered services.
This class utilizes the StreamJsonRpc library when the client and service require RPC to communicate.
StreamJsonRpc applies certain restrictions on the service interface as described here.
A simple calculator service interface may be declared like this:
C#
publicinterfaceICalculator
{
ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken);
ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken);
}
Although the implementation of the methods on this interface may not warrant an async method, we always use async method signatures on this interface because this interface is used to generate the client proxy that may invoke this service remotely, which certainly does warrant an async method signature.
An interface may declare events that can be used to notify their clients of events occurring at the service.
Beyond events or the observer design pattern, a brokered service that needs to "call back" to the client may define a second interface that serves as the contract that a client must implement and provide via the ServiceActivationOptions.ClientRpcTarget property when requesting the service.
Such an interface should conform to all the same design patterns and restrictions as the brokered service interface, but with added restrictions on versioning.
It can be useful to declare this interface in a distinct assembly from the assembly that implements the service so that its clients can reference the interface without the service having to expose more of its implementation detail.
It may also be useful to ship the interface assembly as a NuGet package for other extensions to reference while reserving your own extension to ship the service implementation.
Consider targeting the assembly that declares your service interface to netstandard2.0 to ensure your service may be easily invoked from any .NET process whether it is running .NET Framework, .NET Core, .NET 5 or later.
Testing
Automated tests should be written alongside your service interface to verify the interface's RPC readiness.
The tests should verify that all data passed through the interface are serializable.
Assert that every argument and the return value were serialized completely.
If you are using the test base class mentioned above, your code might look like this:
Consider testing overload resolution if you declare multiple methods with the same name.
You might add an internal field to your mock service for each method on it that stores arguments for that method so the test method can call the method and then verify that the right method was invoked with the right arguments.
Events
Any events declared on your interface should be tested for RPC readiness too.
Events raised from a brokered service do not cause a test failure if they fail during RPC serialization because events are "fire and forget".
If you are using the test base class mentioned above this behavior is already built into some helper methods and might look like this (with unchanged parts omitted for brevity):
The service class should implement the RPC interface declared in the prior step.
A service may implement IDisposable or any other interfaces beyond the one used for RPC.
The proxy generated on the client only implements the service interface, IDisposable, and possibly a few other select interfaces to support the system, so a cast to other interfaces implemented by the service will fail on the client.
Consider the calculator example used above, which we implement here:
C#
internalclassCalculator : ICalculator
{
public ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken)
{
returnnew ValueTask<double>(a + b);
}
public ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken)
{
returnnew ValueTask<double>(a - b);
}
}
Because the method bodies themselves don't need to be async, we explicitly wrap the return value in a constructed ValueTask<TResult> return type to conform to the service interface.
Implementing the observable design pattern
If you offer an observer subscription on your service interface, it might look like this:
The IObserver<T> argument will typically need to outlive the lifetime of this method call so that the client can continue to receive updates after the method call completes until the client disposes of the returned IDisposable value.
To facilitate this your service class may include a collection of IObserver<T> subscriptions that any updates made to your state would then enumerate to update all subscribers.
Be sure enumeration of your collection is thread-safe with respect to each other and especially with the mutations on that collection that may occur via additional subscriptions or disposals of those subscriptions.
Take care that all updates posted via OnNext retain the order in which state changes were introduced to your service.
All subscriptions should ultimately be terminated with either a call to OnCompleted or OnError to avoid resource leaks on the client and RPC systems.
This includes on service disposal where all remaining subscriptions should be explicitly completed.
Your service class is not required to be disposable, but services that are will be disposed of when the client disposes their proxy to your service or the connection between client and service is lost.
Disposable interfaces are tested for in this order: System.IAsyncDisposable, Microsoft.VisualStudio.Threading.IAsyncDisposable, IDisposable.
Only the first interface from this list that your service class implements will be used to dispose of the service.
Keep thread-safety in mind when considering disposal.
Your Dispose method may be called on any thread while other code in your service is running (for example, if a connection being dropped).
Throwing exceptions
When throwing exceptions, consider throwing LocalRpcException with a specific ErrorCode in order to control the error code received by the client in the RemoteInvocationException.
Supplying clients with an error code can enable them to branch based on the nature of the error better than parsing exception messages or types.
Per the JSON-RPC spec, error codes MUST be greater than -32000, including positive numbers.
Consuming other brokered services
When a brokered service itself requires access to another brokered service, we recommend use of the IServiceBroker that is provided to its service factory, but it is especially important when the brokered service registration sets the AllowTransitiveGuestClients flag.
To conform to this guideline if our calculator service had need of other brokered services to implement its behavior, we would modify the constructor to accept an IServiceBroker:
A new instance of this class will be created for each client that requests the service.
A field on the Calculator class above would store a value that might be unique to each client.
Suppose we add a counter that increments every time an operation is performed:
C#
internalclassCalculator : ICalculator
{
int operationCounter;
public ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken)
{
this.operationCounter++;
returnnew ValueTask<double>(a + b);
}
public ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken)
{
this.operationCounter++;
returnnew ValueTask<double>(a - b);
}
}
Your brokered service should be written to follow thread-safe practices.
When using the recommended ServiceJsonRpcDescriptor, remote connections with clients may include concurrent execution of your service's methods as described in this document.
When the client shares a process and AppDomain with the service, the client might call your service concurrently from multiple threads.
A thread-safe implementation of the above example might use Interlocked.Increment(Int32) to increment the operationCounter field.
Shared state
If there is state that your service will need to share across all its clients, this state should be defined in a distinct class that is instantiated by your VS Package and passed in as an argument to your service's constructor.
Suppose we want the operationCounter defined above to count all operations across all clients of the service.
We would need to lift the field into this new state class:
Now we have an elegant, testable way to manage shared state across multiple instances of our Calculator service.
Later when writing the code to proffer the service we will see how this State class is created once and shared with every instance of the Calculator service.
It is especially important to be thread-safe when dealing with shared state because no assumption can be made around multiple clients scheduling their calls such that they are never made concurrently.
If your shared state class needs to access other brokered services, it should use the global service broker rather than one of the contextual ones assigned to an individual instance of your brokered service.
Using the global service broker within a brokered service carries with it security implications when the ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients flag is set.
Security concerns
Security is a consideration for your brokered service if it is registered with the ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients flag, which exposes it to possible access by other users on other machines that are participating in a shared Live Share session.
A brokered service must have a serializable name and an optional version by which a client may request the service.
A ServiceMoniker is a convenient wrapper for these two pieces of information.
A service moniker is analogous to the assembly-qualified full name of a CLR (Common Language Runtime) type.
It must be globally unique and should therefore include your company name and perhaps your extension name as prefixes to the service name itself.
It may be useful to define this moniker in a static readonly field for use elsewhere:
C#
publicstaticreadonly ServiceMoniker Moniker = new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0"));
While most uses of your service may not use your moniker directly, a client that communicates over pipes instead of a proxy will require the moniker.
While a version is optional on a moniker, providing a version is recommended as it gives service authors more options for maintaining compatibility
with clients across behavioral changes.
The service descriptor
The service descriptor combines the service moniker with the behaviors required to run an RPC connection and create a local or remote proxy.
The descriptor is responsible to effectively convert your RPC interface into a wire protocol.
This service descriptor is an instance of a ServiceRpcDescriptor-derived type.
The descriptor must be made available to all clients that will use a proxy to access this service.
Proffering the service also requires this descriptor.
Visual Studio defines one such derived type and recommends its use for all services: ServiceJsonRpcDescriptor.
This descriptor utilizes StreamJsonRpc for its RPC connections and creates a high-performance local proxy for local services that emulates some of the remote behaviors such as wrapping exceptions thrown by the service in RemoteInvocationException.
The ServiceJsonRpcDescriptor supports configuring the JsonRpc class for JSON or MessagePack encoding of the JSON-RPC protocol.
We recommend MessagePack encoding because it's more compact and can be 10X more performant.
We can define a descriptor for our calculator service like this:
C#
///<summary>/// The descriptor for the calculator brokered service./// Use the <see cref="ICalculator"/> interface for the client proxy for this service.///</summary>publicstaticreadonly ServiceRpcDescriptor CalculatorService = new ServiceJsonRpcDescriptor(
Moniker,
Formatters.MessagePack,
MessageDelimiters.BigEndianInt32LengthHeader,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 })
.WithExceptionStrategy(StreamJsonRpc.ExceptionProcessing.ISerializable);
As you can see above, a choice of formatter and delimiter is available.
As not all combinations are valid, we recommend either of these combinations:
Tip: Expose your descriptor as ServiceRpcDescriptor rather than any derived type you use as an implementation detail.
This gives you more flexibility to change implementation details later without API breaking changes.
Include a reference to your service interface in the xml doc comment on your descriptor to make it easier for users to consume your service.
Also reference the interface your service accepts as the client RPC target, if applicable.
Some more advanced services may also accept or require an RPC target object from the client that conforms to some interface.
For such a case, use a ServiceJsonRpcDescriptor constructor with a Type clientInterface parameter to specify the interface the client should supply an instance of.
Versioning the descriptor
Over time you may want to increment the version of your service.
In such a case you should define a descriptor for each version you wish to support, using a unique version-specific ServiceMoniker for each one.
Supporting multiple versions simultaneously can be good for backward compatibility and can usually be done with just one RPC interface.
Visual Studio follows this pattern with its VisualStudioServices class by defining the original ServiceRpcDescriptor as a virtual property under the nested class that represents the first release that added that brokered service.
When we need to change the wire protocol or add/change functionality of the service, Visual Studio declares an override property in a later versioned nested class that returns a new ServiceRpcDescriptor.
For a service defined and proffered by a Visual Studio extension, it may suffice to declare another descriptor property next to the original.
For example suppose your 1.0 service used the UTF8 (JSON) formatter and you realize that switching to MessagePack would deliver a significant performance benefit.
As changing the formatter is a wire protocol-breaking change, it requires incrementing the brokered service version number and a second descriptor.
The two descriptors together might look like this:
C#
publicstaticreadonly ServiceJsonRpcDescriptor CalculatorService = new ServiceJsonRpcDescriptor(
new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0")),
Formatters.UTF8,
MessageDelimiters.HttpLikeHeaders,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 })
);
publicstaticreadonly ServiceJsonRpcDescriptor CalculatorService1_1 = new ServiceJsonRpcDescriptor(
new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.1")),
Formatters.MessagePack,
MessageDelimiters.BigEndianInt32LengthHeader,
new MultiplexingStream.Options { ProtocolMajorVersion = 3 });
Although we declare two descriptors (and later we'll have to proffer and register two services) that we can do this with just one service interface and implementation, keeping overhead for supporting multiple service versions quite low.
Proffering the service
Your brokered service must be created when a request comes in, which is arranged via a step called proffering the service.
In the example below, we proffer a service using the CalculatorService field declared earlier which is set to an instance of a ServiceRpcDescriptor.
We pass it our service factory, which is a BrokeredServiceFactory delegate.
A brokered service is typically instantiated once per client.
This is a departure from other VS (Visual Studio) services, which are typically instantiated once and shared across all clients.
Creating one instance of the service per client allows for better security as each service and/or its connection can retain per-client state about the authorization level the client operates at, what their preferred CultureInfo is, etc.
As we'll see next, it also allows for more interesting services that accept arguments specific to this request.
Important
A service factory that deviates from this guideline and returns a shared service instance instead of a new one to each client should never have its service implement IDisposable, since the first client to dispose of its proxy will lead to disposal of the shared service instance before other clients are done using it.
In the more advanced case where the CalculatorService constructor requires a shared state object and an IServiceBroker, we might proffer the factory like this:
C#
var state = new CalculatorService.State();
container.Proffer(
CalculatorService,
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService(state, serviceBroker)));
The state local variable is outside the service factory and thus is created only once and shared across all instantiated services.
Still more advanced, if the service required access to the ServiceActivationOptions (for example, to invoke methods on the client RPC target object) that could be passed in as well:
C#
var state = new CalculatorService.State();
container.Proffer(CalculatorService, (moniker, options, serviceBroker, cancellationToken) =>
new ValueTask<object?>(new CalculatorService(state, serviceBroker, options)));
In this case the service constructor might look like this, assuming the ServiceJsonRpcDescriptor were created with typeof(IClientCallbackInterface) as one of its constructor arguments:
This clientCallback field can now be invoked anytime the service wants to invoke the client, until the connection is disposed of.
The BrokeredServiceFactory delegate takes a ServiceMoniker as a parameter in case the service factory is a shared method that creates multiple services or distinct versions of the service based on the moniker.
This moniker comes from the client and includes the version of the service they expect.
By forwarding this moniker to the service constructor, the service may emulate the quirky behavior of particular service versions to match what the client may expect.
When you increment the version on your ServiceMoniker, you must proffer each version of your brokered service that you intend to respond to client requests for.
This is done by calling the IBrokeredServiceContainer.Proffer method with each ServiceRpcDescriptor that you still support.
Proffering your service with a null version will serve as a 'catch all' which will match on any client request for which a precise version match with a registered service does not exist.
For example you may proffer your 1.0 and 1.1 service with specific versions, and also register your service with a null version.
In such cases, clients requesting your service with 1.0 or 1.1 invokes the service factory you proffered for those exact versions, while a client requesting version 8.0 leads to your null-versioned proffered service factory being invoked.
Because the client requested version is provided to the service factory, the factory may then make a decision about how to configure the service for this particular client or whether to return null to signify an unsupported version.
A client request for a service with a null version only matches on a service registered and proffered with a null version.
Consider a case where you have published many versions of your service, several of which are backward compatible and thus may share a service implementation.
We can utilize the catch-all option to avoid having to repeatedly proffer each individual version as follows:
cs
conststring 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.
}));
Registering the service
Proffering a brokered service to the global brokered service container will throw unless the service has first been registered.
Registration provides a means for the container to know in advance which brokered services may be available and which VS Package to load when they are requested in order to execute the proffering code.
This allows Visual Studio to start up quickly, without loading all extensions in advance, yet be able to load the required extension when requested by a client of its brokered service.
The default Audience is ServiceAudience.Process, which exposes your brokered service only to other code within the same process.
By setting ServiceAudience.Local, you opt in to exposing your brokered service to other processes belonging to the same Visual Studio session.
When you increment the version on your ServiceMoniker, you must register each version of your brokered service that you intend to respond to client requests for.
By supporting more than the most recent version of your brokered service, you help maintain backward compatibility for clients of your older brokered service version, which may be especially useful when considering the Live Share scenario where each version of Visual Studio that is sharing the session may be a different version.
Registering your service with a null version will serve as a 'catch all' which will match on any client request for which a precise version with a registered service does not exist.
For example you may register your 1.0 and 2.0 service with specific versions, and also register your service with a null version.
Use MEF to proffer and register your service
This requires Visual Studio 2022 Update 2 or later.
A brokered service may be exported via MEF instead of using a Visual Studio Package as described in the previous two sections.
This has tradeoffs to consider:
Tradeoff
Package proffer
MEF export
Availability
✅ Brokered service is available immediately on VS startup.
⚠️ Brokered service may be delayed in availability till MEF has been initialized in the process. This is usually fast, but can take several seconds when the MEF cache is stale.
Cross-platform readiness
⚠️ Visual Studio for Windows specific code must be authored.
✅ The brokered service in your assembly may be loaded in Visual Studio for Windows as well as Visual Studio for Mac.
To export your brokered service via MEF instead of using VS packages:
Implement the IExportedBrokeredService interface on your brokered service class.
Avoid any main thread dependencies in your constructor or importing property setters. Use the IExportedBrokeredService.InitializeAsync method for initializing your brokered service, where main thread dependencies are allowed.
Apply the ExportBrokeredServiceAttribute to your brokered service class, specifying the information about your service moniker, audience, and any other registration-related information required.
If your class requires disposal, implement IDisposable rather than IAsyncDisposable since MEF owns the lifetime of your service and only supports synchronous disposal.
Ensure your source.extension.vsixmanifest file lists the project containing your brokered service as a MEF assembly.
A brokered service is unique in being able to import a few special exports:
IServiceBroker, which should be used to acquire other brokered services.
ServiceMoniker, which can be useful when you export multiple versions of your brokered service and need to detect which version the client requested.
ServiceActivationOptions, which can be useful when you require your clients to provide special parameters or a client callback target.
AuthorizationServiceClient, which can be useful when you need to perform security checks as described in How to secure a brokered service.
This object does not need to be disposed of by your class, as it will be disposed of automatically when your brokered service is disposed of.
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")]
internalclassMefBrokeredService : IExportedBrokeredService, ICalculator
{
internalstatic 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 });
// IExportedBrokeredServicepublic ServiceRpcDescriptor Descriptor => SharedDescriptor;
[Import]
IServiceBroker ServiceBroker { get; set; } = null!;
[Import]
ServiceMoniker ServiceMoniker { get; set; } = null!;
[Import]
ServiceActivationOptions Options { get; set; }
// IExportedBrokeredServicepublic Task InitializeAsync(CancellationToken cancellationToken)
{
return Task.CompletedTask;
}
public ValueTask<int> AddAsync(int a, int b, CancellationToken cancellationToken = default)
{
returnnew(a + b);
}
public ValueTask<int> SubtractAsync(int a, int b, CancellationToken cancellationToken = default)
{
returnnew(a - b);
}
}
Exporting multiple versions of your brokered service
The ExportBrokeredServiceAttribute can be applied to your brokered service multiple times to offer multiple versions of your brokered service.
Your implementation of the IExportedBrokeredService.Descriptor property should return a descriptor with a moniker that matches the one the client requested.
Consider this example, where the calculator service exported 1.0 with UTF8 formatting, then later adds a 1.1 export in order to enjoy the performance wins of using MessagePack formatting.
Beginning with Visual Studio 2022 Update 12 (17.12), a null versioned service can be exported to match any client request for the service regardless of version including a request with a null version.
Such a service can return null from the Descriptor property in order to reject a client request when it doesn't offer an implementation of the version the client requested.
Rejecting a service request
A brokered service can reject a client's activation request by throwing from the InitializeAsync method.
Throwing causes a ServiceActivationFailedException to be thrown back to the client.
Understand and implement dependency injection in an ASP.NET Core app. Use ASP.NET Core's built-in service container to manage dependencies. Register services with the service container.