Compartir a través de


Consumo de un servicio asincrónica

En este documento se describen todo el código, los patrones y las precauciones pertinentes para la adquisición, el uso general y la eliminación de cualquier servicio asincrónica. Para aprender a usar un servicio asincrónica determinado una vez adquirido, busque la documentación concreta de ese servicio asincrónica.

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

Recuperación de un IServiceBroker

Para adquirir un servicio asincrónica, primero debe tener una instancia de IServiceBroker. Cuando el código se ejecuta en el contexto de MEF o un paquete de VS, normalmente se desea el agente de servicio global.

Los propios servicios asincrónicas deben usar el IServiceBroker que se les asigna cuando se invoca su generador de servicios.

El agente de servicio global

Visual Studio ofrece dos maneras de adquirir el agente de servicio global.

Use GlobalProvider.GetServiceAsync para solicitar :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 servicio global:

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

Observe el typeof argumento para el atributo Import, que es necesario.

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

Importante

Las implementaciones de IServiceBroker no suelen implementar IDisposable, pero estos objetos no se pueden recopilar mientras AvailabilityChanged existen controladores. Asegúrese de equilibrar la adición o eliminación de controladores de eventos, especialmente cuando el código pueda descartar el agente de servicio durante la vigencia del proceso.

Agentes de servicios específicos del contexto

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

Los servicios asincrónicas se activan con sus propios IServiceBroker servicios y deben usar esta instancia para cualquiera de sus propias necesidades de servicio asincrónicas, incluidos los servicios prestados con Proffer. Este código proporciona un BrokeredServiceFactory que recibe un agente de servicio que va a usar el servicio asincrónica creado por instancias.

Recuperación de un proxy de servicio asincrónica

La recuperación de un servicio asincrónica normalmente se realiza con el GetProxyAsync método .

El GetProxyAsync método requerirá y ServiceRpcDescriptor la interfaz de servicio como argumento de tipo genérico. La documentación sobre el servicio asincrónica que solicita debe indicar dónde obtener el descriptor y qué interfaz usar. Para los servicios asincrónicas incluidos en Visual Studio, la interfaz que se va a usar debe aparecer en la documentación de IntelliSense en el descriptor. Obtenga información sobre cómo buscar descriptores para servicios asincrónicas de Visual Studio en Detección de servicios asincrónicas 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 asincrónicas, el código anterior activará una nueva instancia de un servicio asincrónica. Después de usar el servicio, el código anterior elimina el proxy a medida que la ejecución sale del using bloque.

Importante

Todos los proxy recuperados deben eliminarse, incluso si la interfaz de servicio no deriva de IDisposable. La eliminación es importante porque el proxy suele tener recursos de E/S que lo respaldan que impiden que se recopilen elementos no utilizados. La eliminación finaliza la E/S, lo que permite que el proxy se recolecte de elementos no utilizados. Use una conversión condicional para IDisposable eliminar y prepararse para que la conversión no pueda evitar una excepción para null servidores proxy o servidores proxy que realmente no implementen IDisposable.

Asegúrese de instalar el paquete NuGet Microsoft.ServiceHub.Analyzers más reciente y mantener habilitadas las reglas del analizador ISBxxxx para ayudar a evitar dichas pérdidas.

La eliminación del proxy dará lugar a la eliminación del servicio asincrónica dedicado a ese cliente.

Si el código requiere un servicio asincrónica y no puede completar su trabajo cuando el servicio no está disponible, puede que desee mostrar un cuadro de diálogo de error al usuario si el código posee la experiencia del usuario en lugar de producir una excepción.

Destinos RPC de cliente

Algunos servicios asincronados aceptan o requieren un destino RPC de cliente para "devoluciones de llamada". Esta opción o requisito debe estar en la documentación de ese servicio asincrónica concreto. Para los servicios asincrónicas de Visual Studio, esta información debe incluirse en la documentación de IntelliSense en el descriptor.

En tal caso, un cliente puede proporcionar uno mediante ServiceActivationOptions.ClientRpcTarget este procedimiento:

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

Invocación del proxy de cliente

El resultado de solicitar un servicio asincrónica es 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 comportamiento 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.

ActionBlock<TInput> Se puede adaptar para implementar IObserver<T> con el método de AsObserver extensión. La clase System.Reactive.Observer del marco reactivo es otra alternativa a implementar la interfaz usted mismo.

Excepciones producidas desde el proxy

  • Se espera RemoteInvocationException que se produzca cualquier excepción producida desde el servicio asincronizado. La excepción original se puede encontrar en .InnerException Este es un comportamiento natural para un servicio hospedado de forma remota porque es el comportamiento de JsonRpc. Cuando el servicio es local, el proxy local ajusta todas las excepciones de la misma manera para que el código de cliente pueda tener solo una ruta de acceso de excepción que funcione para los servicios locales y remotos.
    • Compruebe la ErrorCode propiedad si la documentación del servicio sugiere que se establecerán códigos específicos en función de condiciones específicas en las que pueda bifurcarse.
    • Un conjunto más amplio de errores se comunica detectando RemoteRpcException, que es el tipo base para .RemoteInvocationException
  • Se espera ConnectionLostException que se produzca desde cualquier llamada cuando la conexión a un servicio remoto se quite o el proceso que hospeda el servicio se bloquee. Esto es principalmente de preocupación cuando el servicio se puede adquirir de forma remota.

Almacenamiento en caché del proxy

Hay algunos gastos en la activación de un servicio asincrónica y un proxy asociado, especialmente cuando el servicio procede de un proceso remoto. Cuando el uso frecuente de un servicio asincronizado garantiza el almacenamiento en caché del proxy en muchas llamadas a una clase, el proxy se puede almacenar en un campo de esa clase. La clase contenedora debe ser descartable y desechar el proxy dentro de su Dispose método. 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 aproximadamente correcto, pero no tiene en cuenta las condiciones de carrera entre Dispose y SayHiAsync. El código tampoco tiene en cuenta los AvailabilityChanged eventos que deben dar lugar a la eliminación del proxy previamente adquirido y la solicitud del proxy la próxima vez que sea necesario.

La ServiceBrokerClient clase está diseñada para controlar estas condiciones de carrera e invalidación para ayudar a simplificar su propio código. Considere este ejemplo actualizado 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 desechar y ServiceBrokerClient cada alquiler de un proxy. Las condiciones de carrera entre la eliminación y el uso del proxy se controlan mediante el ServiceBrokerClient objeto , que eliminará cada proxy almacenado en caché en el momento de su propia eliminación o cuando se haya liberado el último alquiler de ese proxy, lo que ocurra por última vez.

Advertencias importantes sobre el ServiceBrokerClient

Elección entre IServiceBroker y ServiceBrokerClient

Ambos son fáciles de usar y el valor predeterminado probablemente debe ser IServiceBroker.

Category IServiceBroker ServiceBrokerClient
Fácil de usar
Requiere eliminación No
Administra la duración del proxy. No. El propietario debe eliminar el proxy cuando haya terminado de usarlo. Sí, se mantienen vivos y reutilizados 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 proporciona un medio práctico para que obtenga una reutilización rápida y frecuente de un proxy, donde no le importa si el servicio subyacente se cambia debajo de usted entre las operaciones de nivel superior.

Pero si le importan esas cosas y quiere administrar la duración de los servidores proxy usted mismo, o necesita controladores de eventos (lo que implica 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 asincrónicas:

Errores de activación del servicio asincrónica

Cuando un servicio disponible puede satisfacer una solicitud de servicio asincrónica, pero la factoría de servicios produce una excepción no controlada, ServiceActivationFailedException se devuelve al cliente para que pueda comprender y notificar el error al usuario.

Cuando una solicitud de servicio asincrónica no puede coincidir con ningún servicio disponible, null se devuelve al cliente. En tal caso, AvailabilityChanged se generará cuándo y si ese servicio está disponible más adelante.

La solicitud de servicio puede rechazarse porque el servicio no está ahí, pero porque la versión ofrecida es inferior a la solicitada. El plan de reserva puede incluir volver a intentar la solicitud de servicio con versiones anteriores con las que el cliente sabe que existe y puede interactuar con él.

Si o cuando la latencia de todas las comprobaciones de versión con errores se vuelve notable, el cliente puede solicitar el VisualStudioServices.VS2019_4Services.RemoteBrokeredServiceManifest para obtener una idea completa de qué servicios y versiones están disponibles desde un origen remoto.

Control de conexiones eliminadas

Es posible que se produzca un error en un proxy de servicio asincrónica adquirido correctamente debido a una conexión eliminada o a un bloqueo en el proceso que lo hospeda. Después de esta interrupción, cualquier llamada realizada en ese proxy dará lugar a ConnectionLostException que se produzca.

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

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

Cuando se solicita un servicio asincrónico solo durante la ejecución de un método asincrónico, no se recomienda controlar este evento. El evento es más relevante para los clientes que almacenan su proxy durante períodos prolongados, de modo que necesitarían compensar los cambios de servicio y están en una posición para actualizar su 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.

Varios cambios de estado pueden dar lugar a la generación de este evento, entre los que se incluyen:

  • Solución o carpeta que se abre o cierra.
  • Se inicia una sesión de Live Share.
  • Un servicio asincrónica registrado dinámicamente que se acaba de detectar.

Un servicio asincrónica afectado solo da como resultado que este evento se genere a los clientes que han solicitado previamente ese servicio, independientemente de que se haya cumplido 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 experimenta un cambio de disponibilidad, no se generará ningún evento en ese cliente. Más adelante, cuando el servicio A experimenta 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 darán lugar a ninguna notificación adicional a ese cliente. Una vez que el cliente solicita A de nuevo, será apto para recibir la siguiente notificación con respecto a ese servicio.

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

Controla ServiceBrokerClient los eventos de cambio de disponibilidad relativos a los servidores proxy almacenados en caché automáticamente mediante la eliminación de los servidores proxy antiguos cuando se han devuelto los alquileres y se solicita una nueva instancia del servicio cuando y si su propietario solicita una. Esta clase puede simplificar sustancialmente el código cuando el servicio no tiene estado y no requiere que el código adjunte controladores de eventos al proxy.

Recuperación de una canalización de servicio asincrónica

Aunque el acceso a un servicio asincronizado a través de un proxy es la técnica más común y conveniente, en escenarios avanzados 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 asincrónica mediante el GetPipeAsync método . Este método toma un ServiceMoniker elemento en lugar de porque ServiceRpcDescriptor los comportamientos rpc proporcionados por un descriptor no son necesarios. Cuando tenga un descriptor, puede obtener el moniker a través de la ServiceRpcDescriptor.Moniker propiedad .

Aunque las canalizaciones están enlazadas a E/S, no son aptas para la recolección de elementos no utilizados. Evite las fugas de memoria siempre completando estas canalizaciones cuando ya no se usen.

En el fragmento de código siguiente, se activa un servicio asincrónica y el cliente tiene una canalización directa. A continuación, 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();
    }
}

Probar clientes de servicio asincrónica

Los servicios asincronados son una dependencia razonable para simular al probar la extensión. Al simular un servicio asincrónico, se recomienda usar un marco ficticio que implemente la interfaz en su nombre e inserte código que necesite para los miembros específicos que invocará el cliente. Esto permite que las pruebas continúen compilando y ejecutándose sin interrupciones cuando se agregan miembros a la interfaz de servicio asincrónica.

Al usar Microsoft.VisualStudio.Sdk.TestFramework para probar la extensión, la prueba puede incluir código estándar para profferar un servicio ficticio en el que el código de cliente puede consultar y ejecutarse. Por ejemplo, supongamos que quería simular el servicio asincronizado VisualStudioServices.VS2022.FileSystem en las pruebas. Podría proffer el simulacro 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));

El contenedor de servicios asincrónicas simulado no requiere que se registre primero un servicio proffered, ya que Visual Studio sí mismo lo hace.

El código sometido a prueba puede adquirir el servicio asincrónica como normal, salvo que en la prueba obtendrá el simulacro 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);
}