Compartir a través de


Consumo de un servicio de intermediación

En este documento se describen todo el código, los patrones y las medidas de precaución correspondientes para la adquisición, el uso general y la eliminación de cualquier servicio de intermediación. Para saber cómo usar un servicio de intermediación determinado una vez adquirido, busque la documentación concreta de ese servicio de intermediación.

En relación con todo el código de este documento, se recomienda activar la característica de tipos de referencia que aceptan valores NULL de C#.

Recuperación de un IServiceBroker

Para adquirir un servicio de intermediación, primero debe tener una instancia de IServiceBroker. Cuando el código se ejecuta en el contexto de MEF (Managed Extensibility Framework) o VSPackage, normalmente es preferible el agente de servicios global.

Los propios servicios de intermediación deben usar el IServiceBroker que se les asigna cuando se invoca el generador de servicios.

El agente de servicios global

Visual Studio ofrece dos maneras de obtener el agente de servicios global.

Use GlobalProvider.GetServiceAsync para solicitar el SVsBrokeredServiceContainer:

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

A partir de Visual Studio 2022, el código que se ejecuta en una extensión activada por MEF puede importar el agente de servicios global:

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

Fíjese en el argumento typeof en el atributo Import, que es obligatorio.

Cada solicitud del IServiceBroker global genera una nueva instancia de un objeto que actúa como vista del contenedor del servicio de intermediación global. Esta instancia única del agente de servicios permite al cliente recibir eventos AvailabilityChanged únicos para el uso de ese cliente. Se recomienda que cada cliente o clase de la extensión obtenga su propio agente de servicios mediante cualquiera de los métodos anteriores en lugar de obtener una instancia y compartirla en toda la extensión. Este patrón también fomenta el uso de patrones de código seguros en los que un servicio de intermediación no debe usar el agente de servicios global.

Importante

Las implementaciones de IServiceBroker no suelen implementar IDisposable, pero estos objetos no se pueden recopilar mientras existan controladores de AvailabilityChanged. Asegúrese de equilibrar los controladores de evento que agregue o elimine, sobre todo cuando el código pueda descartar el agente de servicios mientras dure el proceso.

Agentes de servicios específicos del contexto

El uso del agente de servicios adecuado es un requisito importante del modelo de seguridad de los servicios de intermediación, especialmente en el contexto de las sesiones de Live Share.

Los servicios de intermediación se activan con sus propios IServiceBroker y deben usar esta instancia para cualquiera de las necesidades propias del servicio de intermediación, incluidos los servicios prestados con Proffer. Este código incluye un BrokeredServiceFactory que recibe un agente de servicios que va a usar el servicio de intermediación creado por una instancia.

Recuperación de un proxy de servicio de intermediación

La recuperación de un servicio de intermediación se suele realizar con el método GetProxyAsync.

El método GetProxyAsync necesita un ServiceRpcDescriptor y la interfaz de servicio como argumento de tipo genérico. La documentación sobre el servicio de intermediación que va a solicitar debe indicar dónde obtener el descriptor y qué interfaz usar. En los servicios de intermediación incluidos en Visual Studio, la interfaz que se va a usar debería aparecer en la documentación de IntelliSense en el descriptor. Descubra cómo buscar descriptores para servicios de intermediación de Visual Studio en Detección de servicios de intermediación disponibles.

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

Al igual que con todas las solicitudes de servicio de intermediación, el código anterior activa una nueva instancia de un servicio de intermediación. Después de usar el servicio, el código anterior elimina el proxy a medida que la ejecución sale del bloque using.

Importante

Todos los proxy recuperados deben eliminarse, aunque la interfaz de servicio no derive de IDisposable. Es importante eliminarlo, porque el proxy suele tener recursos de E/S que lo respaldan y que impiden que se recopilen elementos no utilizados. El proceso de eliminación interrumpe la operación de E/S, lo que permite que se recopile el proxy de elementos no utilizados. Use una conversión condicional en IDisposable para realizar la eliminación y prepararse para que la conversión no pueda evitar una excepción de proxies null o proxies que realmente no implementen IDisposable.

Asegúrese de instalar el último paquete Microsoft.ServiceHub.Analyzers de NuGet para seguir teniendo activadas las reglas del analizador ISBxxxx para evitar dichas pérdidas.

La eliminación del proxy da como resultado la eliminación del servicio de intermediación empleado en ese cliente.

Si el código necesita un servicio de intermediación y no puede completar el trabajo cuando el servicio no está disponible, puede hacer que el usuario vea un cuadro de diálogo del error si el código se encarga de la experiencia del usuario en lugar de generar una excepción.

Destinos de RPC de cliente

Algunos servicios de intermediación aceptan o exigen un destino de RPC de cliente (llamada a procedimiento remoto) en "devoluciones de llamada". Esta opción o requisito debe figurar en la documentación de ese servicio de intermediación concreto. Para los servicios de intermediación de Visual Studio, esta información debe estar recogida en la documentación de IntelliSense en el descriptor.

En tal caso, un cliente puede proporcionar uno mediante ServiceActivationOptions.ClientRpcTarget de esta manera:

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

Invocación del proxy de cliente

Al solicitar un servicio de intermediación, se crea una instancia de la interfaz de servicio implementada por un proxy. Este proxy reenvía llamadas y eventos en cada dirección, con algunas diferencias importantes en el proceso de lo que podría esperarse al llamar al servicio directamente.

Patrón de observador

Si el contrato de servicio toma parámetros de tipo IObserver<T>, puede obtener más información sobre cómo construir este tipo en Cómo implementar un observador.

Se puede adaptar para implementar un ActionBlock<TInput> para implementar IObserver<T> con el método de extensión AsObserver. La clase System.Reactive.Observer de la plataforma Reactive es otra alternativa a la hora de implementar la interfaz por su cuenta.

Excepciones generadas a partir del proxy

  • Es posible que se genere RemoteInvocationException por cualquier excepción creada por el servicio de intermediación. La excepción original se puede encontrar en la InnerException. Este funcionamiento es natural en un servicio hospedado de forma remota porque es cómo funciona JsonRpc. Cuando el servicio es local, el proxy local ajusta todas las excepciones de la misma manera para que el código cliente pueda tener solo una ruta de excepción que funcione en los servicios locales y remotos.
    • Compruebe la propiedad ErrorCode si la documentación del servicio plantea que se crean códigos específicos en función de las condiciones específicas en las que puede ramificarse.
    • Se comunica un conjunto más amplio de errores si se detecta RemoteRpcException, que es el tipo base de la RemoteInvocationException.
  • Es posible que se genere ConnectionLostException en cualquier llamada cuando caiga la conexión de un servicio remoto o se bloquee el proceso que hospeda el servicio. Esto es principalmente preocupante cuando el servicio se puede obtener de forma remota.

Almacenamiento en caché del proxy

Pueden haber implicados algunos costes en la activación de un servicio de intermediación y un proxy asociado, especialmente cuando el servicio procede de un proceso remoto. Cuando el uso frecuente de un servicio de intermediación garantiza el almacenamiento en caché del proxy en muchas llamadas en una clase, el proxy se puede almacenar en un campo de esa clase. La clase contenedora debe ser descartable y eliminar el proxy dentro del método Dispose. Considere este ejemplo:

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

El código anterior es correcto en prácticamente todo, pero no tiene en cuenta las condiciones de carrera entre Dispose y SayHiAsync. El código tampoco tiene en cuenta los eventos AvailabilityChanged que deben dar lugar a la eliminación del proxy obtenido anteriormente y la readquisición del proxy la próxima vez que sea necesario.

La clase ServiceBrokerClient está diseñada para controlar estas condiciones de carrera y de invalidación para simplificar el propio código. Fíjese en este nuevo ejemplo que almacena en caché el proxy mediante esta clase auxiliar:

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

El código anterior sigue siendo responsable de eliminar el ServiceBrokerClient y de los alquileres de un proxy. Las condiciones de carrera entre la eliminación y el uso del proxy se controlan mediante el objeto ServiceBrokerClient, que eliminará cada proxy almacenado en caché en el momento en que este se elimine o cuando se haya liberado el último alquiler del proxy, lo que ocurra después.

Advertencias importantes sobre el ServiceBrokerClient

Qué elegir: IServiceBroker o ServiceBrokerClient

Ambos son fáciles de usar y el valor predeterminado probablemente debería ser IServiceBroker.

Category IServiceBroker ServiceBrokerClient
Fácil de usar
Se necesita eliminación No
Controla la duración del proxy No. El propietario debe eliminar el proxy cuando haya terminado de usarlo. Sí, permanecen activos y se reutilizan siempre que sean válidos.
Aplicable a los servicios sin estado
Aplicable a los servicios con estado No
Adecuado cuando se agregan controladores de eventos al proxy No
Evento que se debe notificar cuando se invalida el proxy antiguo AvailabilityChanged Invalidated

ServiceBrokerClient ofrece un método cómodo para reutilizar un proxy de forma rápida y frecuente, donde no es importante si el servicio subyacente se cambia entre las operaciones de nivel superior. Pero si le da importancia a esto y quiere administrar la duración de los proxies por su cuenta o necesita controladores de eventos (lo que significa que necesita administrar la duración del proxy), debe usar IServiceBroker.

Resistencia a las interrupciones del servicio

Hay algunos tipos de interrupciones de servicio que son posibles con los servicios de intermediación:

Errores de activación del servicio de intermediación

Cuando un servicio disponible puede responder a la solicitud de un servicio de intermediación, pero el generador de servicios crea una excepción no controlada, ServiceActivationFailedException se devuelve al cliente para que pueda conocer el error y avisar de este al usuario.

Cuando una solicitud de servicio de intermediación no tiene relación con ningún servicio disponible, null se devuelve al cliente. En tal caso, se generará AvailabilityChanged siempre que ese servicio esté disponible más adelante.

Es posible que la solicitud del servicio no se rechace porque el servicio no está ahí, sino porque la versión ofrecida es inferior a la solicitada. El plan de reserva podría incluir la opción de reintentar la solicitud del servicio con versiones anteriores que el cliente sabe que existe y con la que puede interactuar.

Siempre que se intensifica la latencia de todas las comprobaciones de versión con errores, el cliente puede solicitar el VisualStudioServices.VS2019_4Services.RemoteBrokeredServiceManifest para tener una visión completa de qué servicios y versiones están disponibles desde un origen remoto.

Control de caídas de conexiones

Es posible que se produzca un error en un proxy de servicio de intermediación obtenido correctamente porque se haya caído una conexión o se haya bloqueado el proceso que lo hospeda. Después de esta interrupción, cualquier llamada realizada en ese proxy generará una ConnectionLostException.

Un cliente de servicio de intermediación puede detectar y reaccionar proactivamente a tales caídas de la conexión mediante el control del evento Disconnected. Para llegar a este evento, se debe convertir un proxy en IJsonRpcClientProxy para obtener el objeto JsonRpc. Esta conversión se debe realizar condicionalmente para que se produzca un error correctamente cuando el servicio sea 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.
    }
}

Control de cambios de disponibilidad del servicio

Los clientes de servicio de intermediación pueden recibir notificaciones sobre cuándo deben volver a consultar un servicio de intermediación al que consultaron anteriormente mediante el control del evento AvailabilityChanged. Los controladores en este evento deben agregarse antes de solicitar un servicio de intermediación para asegurarse de que un evento generado poco después de hacerse una solicitud de servicio no se pierda debido a una condición de carrera.

Si se solicita un servicio de intermediación solo durante la ejecución de un método asíncrono, no se recomienda controlar este evento. El evento es más útil para los clientes que almacenan su proxy durante largos períodos, de modo que necesitarían compensar los cambios del servicio y están en una posición para actualizar el proxy.

Este evento se puede generar en cualquier subproceso, posiblemente al mismo tiempo que el código que usa un servicio que describe el evento.

Hay varios cambios de estado que pueden hacer que se genere este evento, como:

  • Una solución o carpeta que se abre o se cierra.
  • El inicio de una sesión de Live Share.
  • Un servicio de intermediación registrado dinámicamente que se acaba de detectar.

Un servicio de intermediación afectado solo hace que se genere este evento en los clientes que han solicitado previamente ese servicio, independientemente de que se haya atendido o no esa solicitud.

El evento se genera como máximo una vez por servicio después de cada solicitud de ese servicio. Por ejemplo, si el cliente solicita el servicio A y el servicio B sufre un cambio de disponibilidad, no se generará ningún evento en ese cliente. Más adelante, cuando el servicio A sufre un cambio de disponibilidad, el cliente recibirá el evento. Si el cliente no vuelve a solicitar el servicio A, los cambios de disponibilidad posteriores de A no activarán ninguna notificación adicional para el cliente. Una vez que el cliente solicita A de nuevo, podrá recibir la siguiente notificación con respecto a ese servicio.

El evento se genera cuando un servicio está disponible, ya no está disponible o sufre un cambio de implementación donde es necesario que todos los clientes del servicio anteriores vuelvan a consultar el servicio.

El ServiceBrokerClient controla automáticamente los eventos de cambio de disponibilidad relativos a los proxies almacenados en caché mediante la eliminación de los proxies antiguos cuando se hayan devuelto los alquileres y solicitando una nueva instancia del servicio cuando el propietario solicita una. Esta clase puede hacer mucho más sencillo el código cuando el servicio no tiene ningún estado y no es necesario que el código adjunte controladores de eventos al proxy.

Recuperación de una canalización de servicio de intermediación

Aunque el acceso a un servicio de intermediación a través de un proxy es la técnica más común y práctica, en situaciones complejas puede ser preferible o necesario solicitar una canalización a ese servicio para que el cliente pueda controlar la RPC directamente o comunicar cualquier otro tipo de datos directamente.

Se puede obtener una canalización al servicio de intermediación mediante el método GetPipeAsync. Este método toma un ServiceMoniker en lugar de un ServiceRpcDescriptor porque las acciones de RPC facilitadas por un descriptor no son necesarias. Cuando tenga un descriptor, puede obtener el moniker a través de la propiedad ServiceRpcDescriptor.Moniker.

Aunque las canalizaciones están vinculadas a E/S, no se pueden usar para la recopilación de elementos no utilizados. Evite las pérdidas de memoria finalizando siempre estas canalizaciones cuando ya no se usen.

En el fragmento de código siguiente, hay activado un servicio de intermediación y el cliente tiene una canalización directa. Luego, el cliente envía el contenido de un archivo al servicio y se desconecta.

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

Pruebas de clientes de servicios de intermediación

Los servicios de intermediación son una dependencia razonable para simular a la hora de probar la extensión. Al simular un servicio de intermediación, se recomienda usar una plataforma ficticia que implemente la interfaz en su nombre e inserte el código que necesite para los miembros específicos que invocará el cliente. Esto permite que las pruebas sigan compilando y ejecutándose sin interrupciones cuando se agregan miembros a la interfaz del servicio de intermediación.

Al usar Microsoft.VisualStudio.Sdk.TestFramework para probar la extensión, la prueba puede incluir código estándar para ofrecer un servicio ficticio donde el código de cliente puede consultar y ejecutarse. Por ejemplo, supongamos que quiere simular el servicio de intermediación VisualStudioServices.VS2022.FileSystem en las pruebas. Podría activar la simulación con este código:

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

Con el contenedor de servicios de intermediación simulado no hay que registrar primero un servicio prestado, ya que Visual Studio lo hace por su cuenta.

El código sometido a prueba puede obtener el servicio de intermediación como normal, salvo que en la prueba llegue la simulación, en lugar del real que obtendría mientras se ejecutaba en 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);
}