Proporcionar un servicio intermediado
Un servicio intermediado consiste en los siguientes elementos:
- interfaz que declara la funcionalidad del servicio y actúa como un contrato entre el servicio y sus clientes.
- Una implementación de esa interfaz.
- Un moniker de servicio para asignar un nombre y una versión al servicio.
- Un descriptor que combina el nombre de servicio con el comportamiento para gestionar RPC (llamada a procedimiento remoto) cuando sea necesario.
- Ya sea ofrecer la fábrica de servicios y registrar su servicio intermediado con un paquete de Visual Studio, o bien hacer ambos con MEF (Managed Extensibility Framework).
Cada uno de los elementos de la lista anterior se describe en detalle en las secciones siguientes.
Con todo el código de este artículo, 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 .NET estándar (a menudo escrita en C#), pero debe cumplir las directrices establecidas por el tipo derivado de ServiceRpcDescriptorque usará el servicio 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 la prohibición de propiedades e indizadores, y que la mayoría o todos los métodos devuelvan Task
u otro tipo de valor devuelto compatible con asincronicidad.
El ServiceJsonRpcDescriptor es el tipo derivado recomendado para los servicios intermediados. Esta clase utiliza la biblioteca StreamJsonRpc 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.
eventos Beyond o el patrón de diseño del observador, un servicio asincrónico que necesita "devolver la llamada" al cliente puede definir una segunda interfaz que actúa como contrato que un cliente debe implementar y proporcionar a través de la propiedad ServiceActivationOptions.ClientRpcTarget al solicitar el servicio. Esta interfaz debe ajustarse a todos los mismos patrones de diseño y restricciones que la interfaz de servicio intermediado, pero con restricciones agregadas en el versionado.
Revise procedimientos recomendados para diseñar un servicio asincrónico para obtener sugerencias sobre cómo diseñar una interfaz RPC eficaz y preparada para el 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 al que otras extensiones hagan referencia mientras reserva su propia extensión para enviar la implementación del servicio.
Considere la posibilidad de establecer como destino el ensamblado que declara la interfaz de servicio para netstandard2.0
para 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.
Ensayo
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 resultar útil la clase BrokeredServiceContractTestBase<TInterface,TServiceMock> del paquete Microsoft.VisualStudio.Sdk.TestFramework.Xunit como base para crear su clase de prueba de interfaz derivada. 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 usa la clase base de prueba mencionada anteriormente, el código 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 campo internal
al servicio ficticio para cada método en él que almacena 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 asegurarse de que estén listos para RPC. Los eventos generados a partir de un servicio intermediado no provocan un fallo en la prueba si fallan durante la serialización RPC porque los eventos se ejecutan y no requieren seguimiento.
Si usa 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 implementa la interfaz de servicio, IDisposabley, posiblemente, algunas otras interfaces selectas para admitir el sistema, por lo que se producirá un error en la conversión a otras interfaces implementadas por el servicio 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 de 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 argumento IObserver<T> 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 de IDisposable devuelto. Para facilitar esto, su clase de servicio puede incluir una colección de suscripciones de IObserver<T> que enumeran actualizaciones realizadas en su estado para actualizar a todos los suscriptores. Asegúrese de que la enumeración de su colección sea segura para subprocesos con respecto entre sí y, especialmente, con las modificaciones de esa colección que puedan ocurrir a través de suscripciones o eliminaciones adicionales de esas suscripciones.
Asegúrese de que todas las actualizaciones publicadas a través de OnNext conserven el orden en el que se introdujeron los cambios de estado en su servicio.
En última instancia, todas las suscripciones deben finalizarse 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 observador, cómo implementar un proveedor de datos observable y, especialmente, teniendo en cuenta RPC.
Servicios descartables
No es necesario que la clase de servicio sea desechable, pero los servicios que lo sean se eliminarán cuando el cliente deseche su proxy al servicio o se pierda la conexión entre el cliente y el servicio. Las interfaces desechables 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 puede llamar al método Dispose en cualquier subproceso mientras se ejecuta otro código del servicio (por ejemplo, si se quita una conexión).
Producir excepciones
Al producirse excepciones, considere la posibilidad de iniciar LocalRpcException con un ErrorCode específico para controlar el código de error recibido por el cliente en el 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 de JSON-RPC, los códigos de error DEBEN ser mayores que -32000, incluidos los números positivos.
Consumo de otros servicios intermediados
Cuando un servicio asincrónico requiere acceso a otro servicio asincrónico, se recomienda usar el IServiceBroker que se proporciona a su generador de servicios, pero es especialmente importante cuando el registro del servicio asincrónico establece la marca AllowTransitiveGuestClients.
Para cumplir con esta directriz, si nuestro servicio de calculadora necesitara otros servicios intermediados 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ónico y Consumo de servicios asincrónicos.
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 clase Calculator
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 asincrónico debe escribirse para seguir prácticas seguras para subprocesos.
Al usar el ServiceJsonRpcDescriptor recomendado, 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 hilos del ejemplo anterior podría usar Interlocked.Increment(Int32) para incrementar el campo operationCounter
.
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 de 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 verificable de administrar el estado compartido en varias instancias de nuestro servicio Calculator
.
Más adelante al escribir el código para proffer el servicio veremos cómo se crea esta clase State
una vez y se comparte con cada instancia del servicio Calculator
.
Es especialmente importante que sea seguro para subprocesos cuando se trabaja con un estado compartido, ya que no se puede suponer que varios clientes programen sus llamadas para que no se realicen simultáneamente.
Si la clase de estado compartido necesita tener acceso a otros servicios asincrónicos, debe usar el agente de servicios global en lugar de uno de los contextuales asignados a una instancia individual del servicio asincrónico. El uso del agente de servicio global dentro de un servicio asincrónico conlleva implicaciones de seguridad cuando se establece la marca ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients.
Problemas de seguridad
Seguridad es una consideración para el servicio de intermediación si está registrado con la marca ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients, que lo expone al posible acceso por parte de otros usuarios en otras máquinas que participan en una sesión compartida de Live Share.
Revise Cómo proteger un servicio intermediado y tome las medidas de seguridad necesarias antes de establecer la bandera AllowTransitiveGuestClients.
El nombre de servicio
Un servicio intermediado debe tener un nombre serializable y una versión opcional a través de la cual un cliente pueda solicitar el servicio. Un ServiceMoniker es un contenedor conveniente para estos dos fragmentos de información.
Un identificador de servicio es análogo al nombre completo calificado en ensamblado de un tipo CLR (Common Language Runtime). 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 campo de static readonly
para su uso en otro lugar:
public static readonly ServiceMoniker Moniker = new ServiceMoniker("YourCompany.Extension.Calculator", new Version("1.0"));
Aunque en la mayoría de los casos su servicio no utilice el moniker directamente, un cliente que se comunique a través de canalizaciones en lugar de un proxy requerirá el moniker.
Aunque una versión es opcional en un moniker, se recomienda proporcionar una versión, ya que ofrece a los autores de servicios más opciones para mantener la compatibilidad con los clientes durante distintas variaciones de comportamiento.
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 tipo derivado de ServiceRpcDescriptor. El descriptor debe estar disponible para todos los clientes que usarán un proxy para acceder a este servicio. Ofrecer el 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 encapsular las excepciones lanzadas por el servicio en RemoteInvocationException.
El ServiceJsonRpcDescriptor admite la configuración de la clase JsonRpc 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 | La mejor opción para |
---|---|---|
MessagePack | BigEndianInt32LengthHeader | Alto rendimiento |
UTF8 (JSON) | HttpLikeHeaders | Interoperabilidad con otros sistemas de JSON-RPC |
Al especificar el objeto MultiplexingStream.Options
como parámetro final, la conexión RPC compartida entre el cliente y el servicio es solo un canal en un MultiplexingStream, compartida con la conexión JSON-RPC a para permitir una transferencia eficaz de grandes volúmenes de datos binarios a través de JSON-RPC.
La estrategia de ExceptionProcessing.ISerializable hace que las excepciones producidas desde el servicio se serialicen y conserven como Exception.InnerException a RemoteInvocationException iniciado 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 su interfaz de servicio en el comentario del documento XML en su descriptor para facilitar que los usuarios consuman su 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.
En tal caso, use un constructor de ServiceJsonRpcDescriptor con un parámetro Type clientInterface
para especificar la interfaz de la que el cliente debe proporcionar una instancia.
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, usando un ServiceMoniker específico para cada versión. 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 clase VisualStudioServices definiendo el ServiceRpcDescriptor original como una propiedad virtual
bajo la clase anidada que representa la primera versión que agregó ese servicio asincrónico.
Cuando es necesario cambiar el protocolo de conexión o agregar o cambiar la funcionalidad del servicio, Visual Studio declara una propiedad override
en una versión posterior de la clase anidada 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 disruptivo en el protocolo de comunicación, requiere incrementar el número de versión del servicio asincrónico 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 más tarde tendremos que ofrecer y registrar dos servicios), podemos hacer esto con una sola interfaz de servicio e implementación, manteniendo la sobrecarga para admitir varias versiones de servicio bastante baja.
Ofreciendo el servicio
El servicio asincrónico debe crearse cuando entra una solicitud, que se organiza a través de un paso denominado ofrecer el servicio.
Fábrica de servicios
Use GlobalProvider.GetServiceAsync para solicitar SVsBrokeredServiceContainer. A continuación, llame a IBrokeredServiceContainer.Proffer en ese contenedor para ofrecer su servicio.
En el ejemplo siguiente, ofrecemos un servicio mediante el campo CalculatorService
declarado anteriormente, que se configura en una instancia de un 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, un servicio intermediado se instancia una vez por cliente. Se trata de una desviación al compararse con otros servicios de VS (Visual Studio), que normalmente se instancian 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, cuál es su CultureInfo preferido, etc. Como veremos a continuación, también permite servicios más interesantes que aceptan argumentos específicos de esta solicitud.
Importante
Un generador 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 hacer que su servicio implemente IDisposable, ya que el primer cliente que elimine su proxy dará lugar a la eliminación de la instancia de servicio compartido antes de que otros clientes terminen de usarla.
En el caso más avanzado en el que el constructor de CalculatorService
requiere un objeto de estado compartido y un IServiceBroker, podríamos ofrecer la función de 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 variable local state
está fuera de la fábrica de servicios y, por tanto, solo se crea una vez y se comparte entre todos los servicios instanciados.
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 podría pasarse:
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 el ServiceJsonRpcDescriptor se creó 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 campo clientCallback
ahora se puede invocar siempre que el servicio quiera invocar al cliente, hasta que se elimine la conexión.
El delegado de BrokeredServiceFactory toma un ServiceMoniker como parámetro para el caso de que la factoría de servicios sea un método compartido que cree varios servicios o versiones distintas del servicio en función del nombre. 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 delegado AuthorizingBrokeredServiceFactory con el método IBrokeredServiceContainer.Proffer a menos que use el IAuthorizationService dentro de su clase de servicio intermediario. Este IAuthorizationService debe eliminarse con su clase de servicio intermediada para evitar una fuga de memoria.
Compatibilidad con varias versiones del servicio
Al incrementar la versión de ServiceMoniker, debe ofrecer cada versión del servicio asincrónico para la que piensa responder a las solicitudes de cliente. Para ello, llame al método IBrokeredServiceContainer.Proffer con cada ServiceRpcDescriptor que siga admitiendo.
Ofrecer su servicio con una versión null
servirá como una solución general que coincidirá con cualquier solicitud de cliente para la cual no exista una coincidencia precisa de versión con un servicio registrado.
Por ejemplo, puede ofrecer sus servicios 1.0 y 1.1 en versiones específicas y también registrar su servicio con una versión null
.
En tales casos, los clientes que solicitan el servicio con la versión 1.0 o 1.1 invocan la fábrica de servicios que ofrecía para esas versiones exactas, mientras que un cliente que solicita la versión 8.0 hace que se invoque una fábrica de servicios sin versión específica.
Dado que la versión solicitada por el cliente se proporciona a la factoría de servicios, la factoría puede tomar una decisión sobre cómo configurar el servicio para este cliente determinado o si se devuelve null
para firmar una versión no admitida.
Una solicitud de un cliente para un servicio con una versión null
, solo coincidirá con un servicio registrado y ofrecido con una versión null
.
Considere un caso en el que haya publicado muchas versiones del servicio, varias de las cuales son compatibles con versiones anteriores y, por tanto, pueden compartir una implementación de servicio. Podemos usar la opción general para evitar tener que ofrecer repetidamente cada versión individual como se indica a continuación:
const string ServiceName = "YourCompany.Extension.Calculator";
ServiceRpcDescriptor CreateDescriptor(Version? version) =>
new ServiceJsonRpcDescriptor(
new ServiceMoniker(ServiceName, version),
ServiceJsonRpcDescriptor.Formatters.MessagePack,
ServiceJsonRpcDescriptor.MessageDelimiters.BigEndianInt32LengthHeader);
IBrokeredServiceContainer container = await AsyncServiceProvider.GlobalProvider.GetServiceAsync<SVsBrokeredServiceContainer, IBrokeredServiceContainer>();
container.Proffer(
CreateDescriptor(new Version(2, 0)),
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(new CalculatorServiceV2()));
container.Proffer(
CreateDescriptor(null), // proffer a catch-all
(moniker, options, serviceBroker, cancellationToken) => new ValueTask<object?>(moniker.Version switch {
{ Major: 1 } => new CalculatorService(), // match any v1.x request with our v1 service.
null => null, // We don't support clients that do not specify a version.
_ => null, // The client requested some other version we don't recognize.
}));
Registro del servicio
Ofrecer un servicio asincrónico al contenedor global de servicios asincrónico producirá una excepción a menos que el servicio se haya registrado previamente. El registro proporciona un medio para que el contenedor conozca de antemano qué servicios asincrónicos pueden estar disponibles y qué paquete de VS cargar cuando se soliciten para ejecutar el código de prestación. 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 ProvideBrokeredServiceAttribute a la clase derivada de AsyncPackage. Este es el único lugar donde se puede establecer ServiceAudience.
[ProvideBrokeredService("YourCompany.Extension.Calculator", "1.0", Audience = ServiceAudience.Local)]
El valor predeterminado de Audience es ServiceAudience.Process, lo que expone el servicio asincrónico solo a otro código dentro del mismo proceso. Al establecer ServiceAudience.Local, opta por exponer su servicio intermediado a otros procesos que pertenecen a la misma sesión de Visual Studio.
Si el servicio asincrónico debe exponerse a los invitados de Live Share, Audience debe incluir ServiceAudience.LiveShareGuest y la propiedad ProvideBrokeredServiceAttribute.AllowTransitiveGuestClients establecerse en true
.
Establecer estas marcas puede introducir vulnerabilidades de seguridad graves y no debe realizarse sin antes seguir las directrices de Cómo asegurar un servicio intermediado.
Al incrementar la versión en su ServiceMoniker, debe registrar cada versión de su servicio asincrónico para la que pretende responder a las solicitudes de los clientes. Al admitir más que solo la versión más reciente de su servicio intermediado, ayuda a mantener la compatibilidad con versiones anteriores para los clientes de la versión anterior de su servicio intermediado, lo que puede ser especialmente útil al considerar el escenario de Live Share, donde cada versión de Visual Studio que comparte la sesión puede ser una versión diferente.
El registro del servicio con una versión de null
servirá como una "solución general" que coincidirá con cualquier solicitud de un cliente donde no exista una versión precisa que tenga un servicio registrado.
Por ejemplo, puede registrar el servicio 1.0 y 2.0 con versiones específicas y también registrar el servicio con una versión de null
.
Uso de MEF para ofrecer y registrar tu servicio
Requiere Visual Studio 2022 Update 2 o posterior.
Un servicio intermediado 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 | Oferta de paquetes | Exportación de MEF |
---|---|---|
Disponibilidad | ✅ El servicio intermediado está disponible inmediatamente al iniciar Visual Studio. | ⚠️ El servicio asincrónico puede retrasar su 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 | ️ ⚠se debe crear código específico de Visual Studio para Windows. | ✅ El servicio asincrónico en el ensamblado se puede cargar en Visual Studio para Windows y en Visual Studio para Mac. |
Para exportar su servicio intermediado a través de MEF en lugar de usar paquetes de Visual Studio:
- 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 ProvideBrokeredServiceAttribute al paquete (si existe).
- Implemente la interfaz
IExportedBrokeredService
en su clase de servicio intermediada. - Evite las dependencias del subproceso principal en su constructor o la importación de los métodos 'setter' de propiedades. Usa el método
IExportedBrokeredService.InitializeAsync
para inicializar tu servicio intermediado, donde se permiten las dependencias del hilo principal. - Aplique el
ExportBrokeredServiceAttribute
a la clase de servicio intermediado, especificando la información sobre el identificador de servicio, la audiencia y cualquier otra información necesaria relacionada con el registro. - Si la clase requiere eliminación, implemente IDisposable en lugar de IAsyncDisposable ya que MEF posee la duración del servicio y solo admite la eliminación sincrónica.
- Asegúrese de que el archivo
source.extension.vsixmanifest
enumera el proyecto que contiene el servicio asincrónico como un ensamblado MEF.
Como parte de MEF, el servicio asincrónico 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 ExportBrokeredServiceAttribute
se deriva de System.ComponentModel.Composition.ExportAttribute y se requiere el mismo espacio de nombres MEF a lo largo de un tipo.
Un servicio asincrónico es único al poder importar algunos elementos especiales:
- IServiceBroker, que se debe usar para adquirir otros servicios asincrónicos.
- ServiceMoniker, que puede ser útil al exportar varias versiones de su servicio intermediado y cuando necesite 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, tal como se describe en Cómo proteger un servicio intermediado. Este objeto no debe ser eliminado por la clase , ya que se eliminará automáticamente cuando se elimine el servicio asincrónico.
El servicio asincrónico no debe usar ImportAttribute de MEF para adquirir otros servicios asincrónicos.
En su lugar, puede [Import]
IServiceBroker y consultar los servicios asincrónicos de la manera tradicional.
Obtenga más información en Cómo consumir un servicio intermediado.
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 de su servicio intermediado
El ExportBrokeredServiceAttribute
se puede aplicar a su servicio intermediado varias veces para ofrecer múltiples versiones de su servicio intermediado.
La implementación de la propiedad IExportedBrokeredService.Descriptor
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!;
}
A partir de Visual Studio 2022 Update 12 (17.12), se puede exportar un servicio con versiones de null
para que coincida con cualquier solicitud de cliente para el servicio independientemente de la versión, incluida una solicitud con una versión de null
.
Este servicio puede devolver null
de la propiedad Descriptor
para rechazar una solicitud de cliente cuando no ofrece una implementación de la versión solicitada por el cliente.
Rechazar una solicitud de servicio
Un servicio asincrónico puede rechazar la solicitud de activación de un cliente lanzando desde el método InitializeAsync. Lanzar provoca que ServiceActivationFailedException se devuelva al cliente.