Share via


Proporcionar un servicio asincrónica

Un servicio asincrónica consta de los siguientes elementos:

  • Interfaz que declara la funcionalidad del servicio y actúa como un contrato entre el servicio y sus clientes.
  • Implementación de esa interfaz.
  • Un moniker de servicio para asignar un nombre y una versión al servicio.
  • Descriptor que combina el moniker de servicio con el comportamiento para controlar RPC cuando sea necesario.
  • Proffer the service factory and register your brokered service with a VS package, or do both with MEF.

Cada uno de los anteriores se describe en detalle a continuación.

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

La interfaz de servicio

La interfaz de servicio puede ser una interfaz de .NET estándar (a menudo escrita en C#), pero debe cumplir las directrices establecidas por el ServiceRpcDescriptortipo derivado de que el servicio usará para asegurarse de que la interfaz se puede usar a través de RPC cuando el cliente y el servicio se ejecutan en distintos procesos. Estas restricciones suelen incluir que no se permiten propiedades e indizadores, y la mayoría o todos los métodos devuelven Task u otro tipo de valor devuelto compatible con asincrónico.

ServiceJsonRpcDescriptor es el tipo derivado recomendado para los servicios asincrónicas. Esta clase utiliza la StreamJsonRpc biblioteca cuando el cliente y el servicio requieren que RPC se comunique. StreamJsonRpc aplica ciertas restricciones en la interfaz de servicio como se describe aquí.

La interfaz puede derivar de IDisposable, System.IAsyncDisposableo incluso, Microsoft.VisualStudio.Threading.IAsyncDisposable pero el sistema no requiere esto. Los servidores proxy de cliente generados implementarán IDisposable de cualquier manera.

Una interfaz de servicio de calculadora simple se puede declarar de la siguiente manera:

public interface ICalculator
{
    ValueTask<double> AddAsync(double a, double b, CancellationToken cancellationToken);
    ValueTask<double> SubtractAsync(double a, double b, CancellationToken cancellationToken);
}

Aunque la implementación de los métodos en esta interfaz puede no garantizar un método asincrónico, siempre usamos firmas de método asincrónico en esta interfaz porque esta interfaz se usa para generar el proxy de cliente que puede invocar este servicio de forma remota, lo que ciertamente garantiza una firma de método asincrónico.

Una interfaz puede declarar eventos que se pueden usar para notificar a sus clientes los eventos que se producen en el servicio.

Más allá de los eventos o el patrón de diseño de observador, un servicio asincrónico que necesita "devolver la llamada" al cliente puede definir una segunda interfaz que actúa como el contrato que un cliente debe implementar y proporcionar a través de la ServiceActivationOptions.ClientRpcTarget propiedad al solicitar el servicio. Esta interfaz debe ajustarse a todos los mismos patrones de diseño y restricciones que la interfaz de servicio asincrónica, pero con restricciones agregadas en el control de versiones.

Revise Los procedimientos recomendados para diseñar un servicio asincrónico para obtener sugerencias sobre cómo diseñar una interfaz RPC eficaz y de prueba de futuro.

Puede ser útil declarar esta interfaz en un ensamblado distinto del ensamblado que implementa el servicio para que sus clientes puedan hacer referencia a la interfaz sin que el servicio tenga que exponer más detalles de su implementación. También puede ser útil enviar el ensamblado de interfaz como un paquete NuGet para que otras extensiones hagan referencia mientras reserva su propia extensión para enviar la implementación del servicio.

Considere la posibilidad de tener como destino el ensamblado que declara la interfaz de servicio para netstandard2.0 asegurarse de que el servicio se pueda invocar fácilmente desde cualquier proceso de .NET, ya sea que ejecute .NET Framework, .NET Core, .NET 5 o posterior.

Prueba

Las pruebas automatizadas deben escribirse junto con la interfaz de servicio para comprobar la preparación de RPC de la interfaz.

Las pruebas deben comprobar que todos los datos que se pasan a través de la interfaz son serializables.

Puede encontrar la BrokeredServiceContractTestBase<TInterface,TServiceMock> clase del paquete Microsoft.VisualStudio.Sdk.TestFramework.Xunit útil para derivar la clase de prueba de interfaz de . Esta clase incluye algunas pruebas de convención básicas para la interfaz, métodos para ayudar con aserciones comunes, como pruebas de eventos, etc.

Métodos

Aserte que todos los argumentos y el valor devuelto se serializaron completamente. Si estaba usando la clase base de prueba mencionada anteriormente, podría tener este aspecto:

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

Considere la posibilidad de probar la resolución de sobrecargas si declara varios métodos con el mismo nombre. Puede agregar un internal campo al servicio ficticio para cada método en él que almacene argumentos para ese método para que el método de prueba pueda llamar al método y, a continuación, comprobar que el método correcto se invocó con los argumentos correctos.

Eventos

Los eventos declarados en la interfaz también deben probarse para la preparación de RPC. Los eventos generados desde un servicio asincrónica no provocarán un error de prueba si fallan durante la serialización RPC porque los eventos son "desencadenados y olvidados".

Si estaba usando la clase base de prueba mencionada anteriormente, este comportamiento ya está integrado en algunos métodos auxiliares y podría tener este aspecto (con partes sin cambios omitidas para mayor brevedad):

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

Implementación del servicio

La clase de servicio debe implementar la interfaz RPC declarada en el paso anterior. Un servicio puede implementar IDisposable o cualquier otra interfaz más allá de la usada para RPC. El proxy generado en el cliente solo implementará la interfaz de servicio, IDisposable, y posiblemente algunas otras interfaces selectas para admitir el sistema, por lo que una conversión a otras interfaces implementadas por el servicio producirá un error en el cliente.

Considere el ejemplo de calculadora usado anteriormente, que implementamos aquí:

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

Dado que los propios cuerpos del método no necesitan ser asincrónicos, encapsulamos explícitamente el valor devuelto en un tipo de valor devuelto construido ValueTask<TResult> para ajustarse a la interfaz de servicio.

Implementación del patrón de diseño observable

Si ofrece una suscripción de observador en la interfaz de servicio, podría tener este aspecto:

Task<IDisposable> SubscribeAsync(IObserver<YourDataType> observer);

El IObserver<T> argumento normalmente tendrá que sobrevivir a la duración de esta llamada de método para que el cliente pueda seguir recibiendo actualizaciones una vez completada la llamada al método hasta que el cliente elimine el valor devuelto IDisposable . Para facilitar esta clase de servicio puede incluir una colección de IObserver<T> suscripciones que las actualizaciones realizadas en su estado se enumerarían para actualizar todos los suscriptores. Asegúrese de que la enumeración de la colección sea segura para subprocesos con respecto al otro y, especialmente, con las mutaciones de esa colección que pueden producirse a través de suscripciones o eliminaciones adicionales de esas suscripciones.

Tenga cuidado de que todas las actualizaciones publicadas a través OnNext de conserven el orden en el que se introdujeron los cambios de estado en el servicio.

Todas las suscripciones deben finalizar en última instancia con una llamada a OnCompleted o OnError para evitar pérdidas de recursos en el cliente y los sistemas RPC. Esto incluye la eliminación del servicio donde se deben completar explícitamente todas las suscripciones restantes.

Obtenga más información sobre el patrón de diseño de observador, cómo implementar un proveedor de datos observable y, especialmente, con RPC en mente.

Servicios descartables

No es necesario que la clase de servicio sea descartable, pero los servicios que se eliminarán cuando el cliente elimine su proxy al servicio o se pierda la conexión entre el cliente y el servicio. Las interfaces descartables se prueban en este orden: System.IAsyncDisposable, Microsoft.VisualStudio.Threading.IAsyncDisposable, IDisposable. Solo la primera interfaz de esta lista que implementa la clase de servicio se usará para eliminar el servicio.

Tenga en cuenta la seguridad de los subprocesos al considerar la eliminación. Se Dispose puede llamar al método en cualquier subproceso mientras se ejecuta otro código del servicio (por ejemplo, si se quita una conexión).

Iniciar excepciones

Al iniciar excepciones, considere la posibilidad de LocalRpcException iniciar con un errorCode específico para controlar el código de error recibido por el cliente en RemoteInvocationException. Proporcionar a los clientes un código de error puede permitirles bifurcarse en función de la naturaleza del error mejor que analizar los tipos o mensajes de excepción.

Según la especificación JSON-RPC, los códigos de error DEBEN ser mayores que -32000, incluidos los números positivos.

Consumo de otros servicios asincrónicas

Cuando un servicio asincrónica requiere acceso a otro servicio asincrónica, se recomienda usar el IServiceBroker que se proporciona a su generador de servicios, pero es especialmente importante cuando el registro del servicio asincrónica establece la AllowTransitiveGuestClients marca.

Para cumplir esta norma si nuestro servicio de calculadora tuviera necesidad de otros servicios asincrónicas para implementar su comportamiento, modificaríamos el constructor para aceptar un 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;
    }

    // ...
}

Obtenga más información sobre cómo proteger un servicio asincrónica y consumir servicios asincrónicas.

Servicios con estado

Estado por cliente

Se creará una nueva instancia de esta clase para cada cliente que solicite el servicio. Un campo de la Calculator clase anterior almacenaría un valor que podría ser único para cada cliente. Supongamos que agregamos un contador que se incrementa cada vez que se realiza una operación:

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

El servicio asincronizado debe escribirse para seguir los procedimientos seguros para subprocesos. Cuando se usa la opción recomendada ServiceJsonRpcDescriptor, las conexiones remotas con clientes pueden incluir la ejecución simultánea de los métodos del servicio, tal como se describe en este documento. Cuando el cliente comparte un proceso y AppDomain con el servicio, el cliente podría llamar al servicio simultáneamente desde varios subprocesos. Una implementación segura para subprocesos del ejemplo anterior podría usarse Interlocked.Increment(Int32) para incrementar el operationCounter campo.

Estado compartido

Si hay un estado en el que el servicio tendrá que compartir en todos sus clientes, este estado debe definirse en una clase distinta creada por el paquete de VS y pasada como argumento al constructor del servicio.

Supongamos que queremos que el operationCounter definido anteriormente cuente todas las operaciones en todos los clientes del servicio. Tendríamos que elevar el campo a esta nueva clase de estado:

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

Ahora tenemos una manera elegante y probable de administrar el estado compartido en varias instancias de nuestro Calculator servicio. Más adelante al escribir el código para proffer el servicio veremos cómo se crea esta State clase una vez y se comparte con cada instancia del Calculator servicio.

Es especialmente importante ser seguro para subprocesos cuando se trabaja con el estado compartido, ya que no se puede realizar ninguna suposición en torno a varios clientes que programan sus llamadas de forma que nunca se realicen simultáneamente.

Si la clase de estado compartido necesita tener acceso a otros servicios asincrónicas, debe usar el agente de servicio global en lugar de uno de los contextuales asignados a una instancia individual del servicio asincrónica. El uso del agente de servicio global dentro de un servicio asincrónica conlleva implicaciones de seguridad cuando se establece la ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients marca.

Problemas de seguridad

La seguridad es una consideración para el servicio asincrónica si está registrada con la ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients marca , que expone a otros usuarios el acceso posible en otras máquinas que participan en una sesión compartida de Live Share.

Revise Protección de un servicio asincrónica y tome las mitigaciones de seguridad necesarias antes de establecer la AllowTransitiveGuestClients marca.

El moniker de servicio

Un servicio asincrónica debe tener un nombre serializable y una versión por los que un cliente puede solicitar el servicio. Un ServiceMoniker es un contenedor conveniente para estos dos fragmentos de información.

Un moniker de servicio es análogo al nombre completo calificado por el ensamblado de un tipo CLR. Debe ser único globalmente y, por tanto, debe incluir el nombre de la empresa y quizás el nombre de la extensión como prefijos para el propio nombre del servicio.

Puede ser útil definir este moniker en un static readonly campo para su uso en otro lugar:

public static readonly ServiceMoniker Moniker = new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0"));

Aunque la mayoría de los usos del servicio pueden no usar el moniker directamente, un cliente que se comunica a través de canalizaciones en lugar de un proxy requerirá el moniker.

Descriptor de servicio

El descriptor de servicio combina el moniker de servicio con los comportamientos necesarios para ejecutar una conexión RPC y crear un proxy local o remoto. El descriptor es responsable de convertir eficazmente la interfaz RPC en un protocolo de conexión. Este descriptor de servicio es una instancia de un ServiceRpcDescriptortipo derivado de . El descriptor debe estar disponible para todos los clientes que usarán un proxy para acceder a este servicio. El proffering del servicio también requiere este descriptor.

Visual Studio define un tipo derivado de este tipo y recomienda su uso para todos los servicios: ServiceJsonRpcDescriptor. Este descriptor utiliza StreamJsonRpc para sus conexiones RPC y crea un proxy local de alto rendimiento para los servicios locales que emula algunos de los comportamientos remotos, como ajustar excepciones producidas por el servicio en RemoteInvocationException.

ServiceJsonRpcDescriptor admite la configuración de la JsonRpc clase para la codificación JSON o MessagePack del protocolo JSON-RPC. Se recomienda codificar MessagePack porque es más compacto y puede ser 10X más eficaz.

Podemos definir un descriptor para nuestro servicio de calculadora como este:

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

Como puede ver anteriormente, hay disponible una opción de formateador y delimitador. Como no todas las combinaciones son válidas, se recomienda cualquiera de estas combinaciones:

ServiceJsonRpcDescriptor.Formatters ServiceJsonRpcDescriptor.MessageDelimiters Más adecuado para
MessagePack BigEndianInt32LengthHeader Alto rendimiento
UTF8 (JSON) HttpLikeHeaders Interoperabilidad con otros sistemas JSON-RPC

Al especificar el MultiplexingStream.Options objeto como parámetro final, la conexión RPC compartida entre el cliente y el servicio es solo un canal en multiplexingStream, que se comparte con la conexión JSON-RPC para permitir la transferencia eficaz de datos binarios grandes a través de JSON-RPC.

La ExceptionProcessing.ISerializable estrategia hace que las excepciones producidas desde el servicio se serialicen y conserven como en Exception.InnerException el RemoteInvocationException que se inicia en el cliente. Sin esta configuración, la información de excepción menos detallada está disponible en el cliente.

Sugerencia: Exponga el descriptor como ServiceRpcDescriptor en lugar de cualquier tipo derivado que use como detalle de implementación. Esto proporciona más flexibilidad para cambiar los detalles de implementación más adelante sin cambios importantes en la API.

Incluya una referencia a la interfaz de servicio en el comentario del documento xml en el descriptor para facilitar a los usuarios el consumo del servicio. También haga referencia a la interfaz que el servicio acepta como destino RPC del cliente, si procede.

Algunos servicios más avanzados también pueden aceptar o requerir un objeto de destino RPC del cliente que se ajuste a alguna interfaz. Para este caso, use un ServiceJsonRpcDescriptor constructor con un Type clientInterface parámetro para especificar la interfaz de la que el cliente debe proporcionar una instancia de .

Control de versiones del descriptor

Con el tiempo, es posible que quiera incrementar la versión del servicio. En tal caso, debe definir un descriptor para cada versión que desee admitir, utilizando una versión única específica ServiceMoniker para cada una. Admitir varias versiones simultáneamente puede ser buena para la compatibilidad con versiones anteriores y normalmente se puede hacer con una sola interfaz RPC.

Visual Studio sigue este patrón con su VisualStudioServices clase definiendo el original ServiceRpcDescriptor como una virtual propiedad bajo la clase anidada que representa la primera versión que agregó ese servicio asincrónica. Cuando es necesario cambiar el protocolo de conexión o agregar o cambiar la funcionalidad del servicio, Visual Studio declara una override propiedad en una clase anidada con versiones posteriores que devuelve un nuevo ServiceRpcDescriptor.

Para un servicio definido y proferido por una extensión de Visual Studio, puede ser suficiente declarar otra propiedad descriptor junto al original. Por ejemplo, supongamos que el servicio 1.0 usó el formateador UTF8 (JSON) y se da cuenta de que cambiar a MessagePack proporcionaría una ventaja significativa de rendimiento. Como cambiar el formateador es un cambio importante en el protocolo de conexión, requiere incrementar el número de versión del servicio asincrónica y un segundo descriptor. Los dos descriptores juntos podrían tener este aspecto:

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

Aunque declaramos dos descriptores (y versiones posteriores tendremos que proffer y registrar dos servicios) que podemos hacer con una sola interfaz de servicio e implementación, manteniendo la sobrecarga para admitir varias versiones de servicio bastante bajas.

Proffering the service (Proffering the service)

El servicio asincrónica debe crearse cuando entra una solicitud, que se organiza a través de un paso denominado proffering el servicio.

Generador de servicios

Use GlobalProvider.GetServiceAsync para solicitar .SVsBrokeredServiceContainer A continuación, llame IBrokeredServiceContainer.Proffer a en ese contenedor para proffer el servicio.

En el ejemplo siguiente, profferemos un servicio mediante el CalculatorService campo declarado anteriormente, que se establece en una instancia de .ServiceRpcDescriptor Lo pasamos a nuestra fábrica de servicios, que es un BrokeredServiceFactory delegado.

IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
container.Proffer(
    CalculatorService,
    (moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService()));

Normalmente, se crea una instancia de un servicio asincrónica por cliente. Se trata de una salida de otros servicios de VS, que normalmente se crean instancias una vez y se comparten entre todos los clientes. La creación de una instancia del servicio por cliente permite una mejor seguridad, ya que cada servicio o su conexión pueden conservar el estado por cliente sobre el nivel de autorización en el que opera el cliente, en lo que es su preferencia CultureInfo , etc. Como veremos a continuación, también permite servicios más interesantes que aceptan argumentos específicos de esta solicitud.

Importante

Una factoría de servicios que se desvía de esta guía y devuelve una instancia de servicio compartida en lugar de una nueva a cada cliente nunca debe tener su servicio implementadoIDisposable, ya que el primer cliente para eliminar su proxy dará lugar a la eliminación de la instancia de servicio compartido antes de que otros clientes lo usen.

En el caso más avanzado en el que el CalculatorService constructor requiere un objeto de estado compartido y un IServiceBroker, podríamos proder la fábrica de la siguiente manera:

var state = new CalculatorService.State();
container.Proffer(
    CalculatorService,
    (moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorService(state, serviceBroker)));

La state variable local está fuera de la factoría de servicio y, por tanto, solo se crea una vez y se comparte en todos los servicios creados por instancias.

Aún más avanzado, si el servicio requería acceso a ServiceActivationOptions (por ejemplo, para invocar métodos en el objeto de destino RPC del cliente) que también se podían pasar:

var state = new CalculatorService.State();
container.Proffer(CalculatorService, (moniker, options, serviceBroker, cancellationToken) =>
    new ValueTask<object?>(new CalculatorService(state, serviceBroker, options)));

En este caso, el constructor de servicio podría tener este aspecto, suponiendo que ServiceJsonRpcDescriptor se crearon con typeof(IClientCallbackInterface) como uno de sus argumentos de constructor:

internal class Calculator(State state, IServiceBroker serviceBroker, ServiceActivationOptions options)
{
    this.state = state;
    this.serviceBroker = serviceBroker;
    this.options = options;
    this.clientCallback = (IClientCallbackInterface)options.ClientRpcTarget;
}

Este clientCallback campo ahora se puede invocar en cualquier momento en que el servicio quiera invocar al cliente, hasta que se elimine la conexión.

Al incrementar la versión en ServiceMoniker, debe proffer cada versión del servicio asincrónica para la que pretende responder a las solicitudes de cliente. Para ello, llame al IBrokeredServiceContainer.Proffer método con cada uno de los ServiceRpcDescriptor que siga admitiendo.

El BrokeredServiceFactory delegado toma como ServiceMoniker parámetro en caso de que la factoría de servicios sea un método compartido que cree varios servicios basados en el moniker. Este moniker procede del cliente e incluye la versión del servicio que esperan. Al reenviar este moniker al constructor de servicio, el servicio puede emular el comportamiento peculiar de determinadas versiones de servicio para que coincidan con lo que puede esperar el cliente.

Evite usar el AuthorizingBrokeredServiceFactory delegado con el IBrokeredServiceContainer.Proffer método a menos que use dentro de la IAuthorizationService clase de servicio asincrónica. Esto IAuthorizationServicese debe eliminar con la clase de servicio asincrónica para evitar una pérdida de memoria.

Registrar el servicio

Proffering a brokered service to the global brokered service container will throw a menos que el servicio se haya registrado por primera vez. El registro proporciona un medio para que el contenedor sepa con antelación qué servicios asincrónicas pueden estar disponibles y qué paquete de VS se carga cuando se solicitan para ejecutar el código de búfer. Esto permite que Visual Studio se inicie rápidamente, sin cargar todas las extensiones de antemano, pero puede cargar la extensión necesaria cuando lo solicite un cliente de su servicio asincronado.

El registro se puede realizar aplicando a la ProvideBrokeredServiceAttributeAsyncPackageclase derivada de . Este es el único lugar donde ServiceAudience se puede establecer .

[ProvideBrokeredService("YourCompany.Extension.Calculator", "1.0", Audience = ServiceAudience.Local)]

El valor predeterminado Audience es ServiceAudience.Process, que expone el servicio asincrónica solo a otro código dentro del mismo proceso. Al establecer ServiceAudience.Local, puede optar por exponer el servicio asincrónica a otros procesos que pertenecen a la misma sesión de Visual Studio.

Si el servicio asincrónica debe exponerse a los invitados de Live Share, Audience debe incluir ServiceAudience.LiveShareGuest y la ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients propiedad establecida trueen . Establecer estas marcas puede introducir vulnerabilidades de seguridad graves y no debe realizarse sin cumplir primero las instrucciones de Cómo proteger un servicio asincrónica.

Al incrementar la versión en ServiceMoniker, debe registrar cada versión del servicio asincrónica para la que pretende responder a las solicitudes de cliente. Al admitir más que la versión más reciente del servicio asincronizado, ayuda a mantener la compatibilidad con versiones anteriores de los clientes de la versión de servicio asincrónica anterior, lo que puede ser especialmente útil al considerar el escenario de Live Share en el que cada versión de Visual Studio que comparte la sesión puede ser una versión diferente.

Uso de MEF para proffer y registrar el servicio

Esto requiere Visual Studio 2022 Update 2 o posterior.

Un servicio asincrónica se puede exportar a través de MEF en lugar de usar un paquete de Visual Studio como se describe en las dos secciones anteriores. Esto tiene inconvenientes que tener en cuenta:

Compensación Proffer de paquetes Exportación de MEF
Disponibilidad ✅ El servicio asincrónica está disponible inmediatamente en el inicio de VS. ⚠️ El servicio asincrónica puede retrasarse en la disponibilidad hasta que MEF se haya inicializado en el proceso. Esto suele ser rápido, pero puede tardar varios segundos cuando la caché MEF está obsoleta.
Preparación multiplataforma ⚠️ Visual Studio para código específico de Windows debe crearse. ✅El servicio asincrónica del ensamblado se puede cargar en Visual Studio para Windows, así como Visual Studio para Mac.

Para exportar el servicio asincrónica a través de MEF en lugar de usar paquetes de VS:

  1. Confirme que no tiene ningún código relacionado con las dos últimas secciones. En concreto, no debe tener ningún código que llame a IBrokeredServiceContainer.Proffer y no debe aplicar al ProvideBrokeredServiceAttribute paquete (si existe).
  2. Implemente la interfaz en la IExportedBrokeredService clase de servicio asincrónica.
  3. Evite las dependencias principales del subproceso en el constructor o importe establecedores de propiedades. Use el método para inicializar el IExportedBrokeredService.InitializeAsync servicio asincrónica, donde se permiten las dependencias de subprocesos principales.
  4. Aplique a la ExportBrokeredServiceAttribute clase de servicio asincrónica, especificando la información sobre el moniker de servicio, la audiencia y cualquier otra información relacionada con el registro necesaria.
  5. Si la clase requiere eliminación, implemente IDisposable en lugar de IAsyncDisposable porque MEF posee la duración del servicio y solo admite la eliminación sincrónica.
  6. source.extension.vsixmanifest Asegúrese de que el archivo muestra el proyecto que contiene el servicio asincrónica como ensamblado MEF.

Como parte de MEF, el servicio asincrónica puede importar cualquier otro elemento MEF en el ámbito predeterminado. Al hacerlo, asegúrese de usar System.ComponentModel.Composition.ImportAttribute en lugar de System.Composition.ImportAttribute. Esto se debe a que se ExportBrokeredServiceAttribute deriva de System.ComponentModel.Composition.ExportAttribute y usa el mismo espacio de nombres MEF a lo largo de un tipo.

Un servicio asincrónica es único para poder importar algunas exportaciones especiales:

  • IServiceBroker, que se debe usar para adquirir otros servicios asincrónicas.
  • ServiceMoniker, que puede ser útil cuando exporta varias versiones del servicio asincrónica y necesita detectar qué versión solicitó el cliente.
  • ServiceActivationOptions, que puede ser útil cuando se requiere que los clientes proporcionen parámetros especiales o un destino de devolución de llamada de cliente.
  • AuthorizationServiceClient, que puede ser útil cuando necesite realizar comprobaciones de seguridad como se describe en Protección de un servicio asincronizado. Esta clase no necesita eliminar este objeto, ya que se eliminará automáticamente cuando se elimine el servicio asincrónica.

El servicio asincrónica no debe usar meF ImportAttribute para adquirir otros servicios asincrónicas. En su lugar, puede [Import]IServiceBroker y consultar los servicios asincrónicas de la manera tradicional. Obtenga más información en Uso de un servicio asincrónica.

Este es un ejemplo:

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

Exportación de varias versiones del servicio asincrónica

ExportBrokeredServiceAttribute Puede aplicarse al servicio asincrónica varias veces para ofrecer varias versiones del servicio asincrónica.

La implementación de la IExportedBrokeredService.Descriptor propiedad debe devolver un descriptor con un moniker que coincida con el que solicitó el cliente.

Considere este ejemplo, donde el servicio de calculadora exportó la versión 1.0 con formato UTF8 y, después, agrega una exportación 1.1 para disfrutar del rendimiento que gana el uso del formato 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!;
}