Consume a brokered service

This document describes all the code, patterns, and cautions relevant to the acquisition, general use, and disposal of any brokered service. To learn to use a particular brokered service once acquired, look up the particular documentation for that brokered service.

With all code in this document, activating C#'s nullable reference types feature is highly recommended.

Retrieving an IServiceBroker

To acquire a brokered service, you must first have an instance of IServiceBroker. When your code is running in the context of MEF or a VS Package, you typically want the global service broker.

Brokered services themselves should use the IServiceBroker that they are assigned when their service factory is invoked.

The global service broker

Visual Studio offers two ways to acquire the global service broker.

Use GlobalProvider.GetServiceAsync to request the SVsBrokeredServiceContainer:

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

Starting with Visual Studio 2022, code running in a MEF activated extension may import the global service broker:

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

Notice the typeof argument to the Import attribute, which is required.

Each request for the global IServiceBroker produces a new instance of an object that serves as a view into the global brokered service container. This unique instance of the service broker allows your client to receive AvailabilityChanged events unique to that client's use. We recommend that each client/class in your extension acquire its own service broker using either of the above approaches rather than acquiring one instance and sharing it across your entire extension. This pattern also encourages secure coding patterns where a brokered service should not be using the global service broker.

Important

Implementations of IServiceBroker do not typically implement IDisposable, but these objects cannot be collected while AvailabilityChanged handlers exist. Be sure to balance add/remove of event handlers, especially when the code may discard the service broker during the lifetime of the process.

Context specific service brokers

Using the appropriate service broker is an important requirement of the security model of brokered services, particularly in the context of Live Share sessions.

Brokered services are activated with their own IServiceBroker and should use this instance for any of their own brokered service needs, including services proffered with Proffer. Such code provides a BrokeredServiceFactory that receives a service broker to be used by the instantiated brokered service.

Retrieving a brokered service proxy

Retrieval of a brokered service is typically done with the GetProxyAsync method.

The GetProxyAsync method will require a ServiceRpcDescriptor and the service interface as a generic type argument. The documentation on the brokered service you are requesting should indicate where to get the descriptor and which interface to use. For brokered services included with Visual Studio, the interface to use should appear in the IntelliSense documentation on the descriptor. Learn how to find descriptors for Visual Studio brokered services in Discovering Available Brokered Services.

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

As with all brokered service requests, the above code will activate a new instance of a brokered service. After using the service, the code above disposes of the proxy as execution exits the using block.

Important

Every proxy retrieved must be disposed of, even if the service interface does not derive from IDisposable. Disposal is important because the proxy often has I/O resources backing it that prevent it from being garbage collected. Disposal terminates the I/O, allowing the proxy to be garbage collected. Use a conditional cast to IDisposable for disposal and be prepared for the cast to fail to avoid an exception for null proxies or proxies that do not actually implement IDisposable.

Be sure to install the latest Microsoft.ServiceHub.Analyzers NuGet package and keep the ISBxxxx analyzer rules enabled to help prevent such leaks.

Disposal of the proxy will result in disposal of the brokered service that was dedicated to that client.

If your code requires a brokered service and cannot complete its work when the service is not available, you may want to display an error dialog to the user if the code owns the user experience rather than throw an exception.

Client RPC targets

Some brokered services accept or require a client RPC target for "callbacks". Such an option or requirement should be in the documentation of that particular brokered service. For Visual Studio brokered services this information should be included in the IntelliSense documentation on the descriptor.

In such case, a client may provide one using ServiceActivationOptions.ClientRpcTarget like this:

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

Invoking the client proxy

The result of requesting a brokered service is an instance of the service interface implemented by a proxy. This proxy forwards calls and events each direction, with some important differences in behavior from what one might expect when calling the service directly.

Observer pattern

If the service contract takes parameters of type IObserver<T>, you may learn more about how to construct such a type at How to implement an observer.

An ActionBlock<TInput> can be adapted to implement IObserver<T> with the AsObserver extension method. The System.Reactive.Observer class from the Reactive framework is another alternative to implementing the interface yourself.

Exceptions thrown from the proxy

  • Expect RemoteInvocationException to be thrown for any exception thrown from the brokered service. The original exception can be found in the InnerException. This is natural behavior for a remotely hosted service because it is behavior from JsonRpc. When the service is local, the local proxy wraps all exceptions in the same way so that the client code can have just one exception path that works for local and remote services.
    • Check the ErrorCode property if the service documentation suggests that specific codes will be set based on specific conditions that you can branch on.
    • A broader set of errors is communicated by catching RemoteRpcException, which is the base type for the RemoteInvocationException.
  • Expect ConnectionLostException to be thrown from any call when the connection to a remote service drops or the process hosting the service crashes. This is primarily of concern when the service may be acquired remotely.

Caching of the proxy

There is some expense in the activation of a brokered service and associated proxy, particularly when the service comes from a remote process. When frequent use of a brokered service warrants caching of the proxy across many calls into a class, the proxy may be stored in a field on that class. The containing class should be disposable and dispose of the proxy inside its Dispose method. Consider this example:

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

The above code is roughly correct, but it does not account for race conditions between Dispose and SayHiAsync. The code also does not account for AvailabilityChanged events that should lead to disposal of the proxy previously acquired and the reacquisition of the proxy the next time it is required.

The ServiceBrokerClient class is designed to handle these race and invalidation conditions to help keep your own code simple. Consider this updated example that caches the proxy using this helper class:

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

The above code is still responsible to dispose of the ServiceBrokerClient and each rental of a proxy. Race conditions between disposal and use of the proxy are handled by the ServiceBrokerClient object, which will dispose of each cached proxy at the time of its own disposal or when the last rental of that proxy has been released, whichever comes last.

Important caveats regarding the ServiceBrokerClient

Choosing between IServiceBroker and ServiceBrokerClient

Both are user-friendly, and the default should probably be IServiceBroker.

Category IServiceBroker ServiceBrokerClient
User friendly Yes Yes
Requires disposal No Yes
Manages lifetime of proxy No. Owner must dispose of proxy when done using it. Yes, they are kept alive and reused as long as they are valid.
Applicable for stateless services Yes Yes
Applicable for stateful services Yes No
Appropriate when event handlers are added to proxy Yes No
Event to notify when old proxy is invalidated AvailabilityChanged Invalidated

ServiceBrokerClient provides a convenient means for you to get fast and frequent reuse of a proxy, where you don't care if the underlying service is changed out from under you in between top-level operations.

But if you do care about those things and want to manage the lifetime of your proxies yourself, or you need event handlers (which implies you need to manage lifetime of the proxy), you should use IServiceBroker.

Resilience to service disruptions

There are a few kinds of service disruptions that are possible with brokered services:

Brokered service activation failures

When a brokered service request can be satisfied by an available service but the service factory throws an unhandled exception, a ServiceActivationFailedException is thrown back to the client so they can understand and report the failure to the user.

When a brokered service request cannot be matched up with any available service, null is returned to the client. In such a case, AvailabilityChanged will be raised when and if that service becomes available later.

The service request may be declined not because the service isn't there, but because the version offered is lower than the version requested. Your fallback plan may include retrying the service request with lower versions that your client knows exist and is able to interact with.

If/when latency from all the failed version checks becomes noticeable, the client can request the VisualStudioServices.VS2019_4Services.RemoteBrokeredServiceManifest to get a complete idea of what services and versions are available from a remote source.

Handling dropped connections

A successfully acquired brokered service proxy may fail due to a dropped connection or a crash in the process that hosts it. After such a disruption, any call made on that proxy will result in ConnectionLostException being thrown.

A brokered service client can proactively detect and react to such connection drops by handling the Disconnected event. To reach this event, a proxy must be cast to IJsonRpcClientProxy to obtain the JsonRpc object. This cast should be made conditionally so as to gracefully fail when the service is local.

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.
    }
}

Handling service availability changes

Brokered service clients may receive notifications of when they should requery for a brokered service they previously queried for by handling the AvailabilityChanged event. Handlers to this event should be added before requesting a brokered service to ensure an event raised soon after a service request is made is not lost due to a race condition.

When a brokered service is requested only for the duration of one async method's execution, handling this event is not recommended. The event is most relevant to clients that store their proxy for extended periods such that they would need to compensate for service changes and are in a position to refresh their proxy.

This event may be raised on any thread, possibly concurrently to code that is using a service that the event is describing.

Several state changes may lead to raising of this event, including:

  • A solution or folder being opened or closed.
  • A Live Share session starting.
  • A dynamically registered brokered service that was just discovered.

An impacted brokered service only results in this event being raised to clients that have previously requested that service, whether or not that request was fulfilled.

The event is raised at most once per service after each request for that service. For example if the client requests service A and service B experiences an availability change, no event will be raised to that client. Later, when service A experiences an availability change, the client will receive the event. If the client does not re-request service A, subsequent availability changes for A will not result in any further notifications to that client. Once the client requests A again, it becomes eligible to receive the next notification regarding that service.

The event is raised when a service becomes available, is no longer available, or experiences an implementation change that requires all prior service clients to requery for the service.

The ServiceBrokerClient handles availability change events regarding cached proxies automatically by disposing of the old proxies when any rentals have been returned and requesting a new instance of the service when and if its owner requests one. This class can substantially simplify your code when the service is stateless and does not require that your code attaches event handlers to the proxy.

Retrieving a brokered service pipe

Although accessing a brokered service through a proxy is the most common and convenient technique, in advanced scenarios it may be preferable or necessary to request a pipe to that service so the client can control the RPC directly or communicate any other data type directly.

A pipe to the brokered service may be obtained via the GetPipeAsync method. This method takes a ServiceMoniker instead of a ServiceRpcDescriptor because the RPC behaviors provided by a descriptor are not required. When you have a descriptor, you can obtain the moniker from it via the ServiceRpcDescriptor.Moniker property.

While pipes are bound to I/O they are not eligible for garbage collection. Avoid memory leaks by always completing these pipes when they will no longer be used.

In the following snippet, a brokered service is activated and the client has a direct pipe to it. The client then sends the content of a file to the service and disconnects.

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

Testing brokered service clients

Brokered services are a reasonable dependency to mock when testing your extension. When mocking a brokered service, we recommend using a mocking framework that implements the interface on your behalf and injects code that you require to the specific members your client will invoke. This allows your tests to continue to compile and run without breaks when members are added to the brokered service interface.

When using the Microsoft.VisualStudio.Sdk.TestFramework to test your extension, your test can include standard code to proffer a mock service which your client code can query for and run against. For example suppose you wanted to mock up the VisualStudioServices.VS2022.FileSystem brokered service in your tests. You could proffer the mock with this code:

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

The mocked brokered service container does not require a proffered service to be registered first as Visual Studio itself does.

Your code under test can acquire the brokered service as normal, except that under the test it will get your mock instead of the real one it would get while running under 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);
}