Procedimientos recomendados para diseñar un servicio de intermediación
Siga las instrucciones y restricciones generales documentadas para las interfaces RPC para StreamJsonRpc.
Además, se aplican las siguientes directrices a los servicios asincrónicas.
Firmas de método
Todos los métodos deben tomar un CancellationToken parámetro como último parámetro. Este parámetro normalmente no debe ser un parámetro opcional, por lo que es menos probable que los autores de llamadas omitan accidentalmente el argumento. Incluso si se espera que la implementación del método sea trivial, lo que permite CancellationToken al cliente cancelar su propia solicitud antes de que se transmita al servidor. También permite que la implementación del servidor evolucione en algo más caro sin tener que actualizar el método para agregar la cancelación como una opción más adelante.
Considere la posibilidad de evitar varias sobrecargas del mismo método en la interfaz RPC. Aunque la resolución de sobrecarga normalmente funciona (y las pruebas deben escribirse para comprobar que sí), se basa en intentar deserializar argumentos basados en los tipos de parámetro de cada sobrecarga, lo que da lugar a que se produzcan excepciones de primera oportunidad como parte normal de la selección de una sobrecarga. Como queremos minimizar el número de excepciones de primera oportunidad producidas en rutas de acceso correctas, es preferible simplemente tener un método con un nombre determinado.
Tipos de parámetro y valor devuelto
Recuerde que todos los argumentos y valores devueltos intercambiados a través de RPC son solo datos. Todos se serializan y envían a través de la conexión. Los métodos que defina en estos tipos de datos solo operan en esa copia local de los datos y no tienen forma de comunicarse con el servicio RPC que lo generó. Las únicas excepciones a este comportamiento de serialización son los tipos exóticos para los que StreamJsonRpc tiene compatibilidad especial.
Considere la posibilidad de usar ValueTask<T>
over Task<T>
como el tipo de valor devuelto de métodos, ya que ValueTask<T>
incurre en menos asignaciones.
Al usar la variedad no genérica (por ejemplo, Task y ValueTask) es menos importante, pero ValueTask puede ser preferible.
Tenga en cuenta las restricciones de uso en ValueTask<T>
tal y como se documenta en esa API. Esta entrada de blog y vídeo puede ser útil para decidir qué tipo usar también.
Tipos de datos personalizados
Considere la posibilidad de definir todos los tipos de datos para que sean inmutables, lo que permite un uso compartido más seguro de los datos en un proceso sin copiar y ayuda a reforzar la idea a los consumidores de que no pueden cambiar los datos que reciben en respuesta a una consulta sin colocar otro RPC.
Defina los tipos de datos como class
en lugar de struct
al usar ServiceJsonRpcDescriptor.Formatters.UTF8, lo que evita el costo de la conversión boxing (potencialmente repetida) al usar Newtonsoft.Json.
La conversión boxing no se produce cuando se usa ServiceJsonRpcDescriptor.Formatters.MessagePack para que las estructuras sean una opción adecuada si se confirma en ese formateador.
Considere la posibilidad de implementar IEquatable<T> y invalidar GetHashCode() y Equals(Object) métodos en los tipos de datos, lo que permite al cliente almacenar, comparar y reutilizar de forma eficaz los datos recibidos en función de si es igual a los datos recibidos en otro momento.
Use para admitir la DiscriminatedTypeJsonConverter<TBase> serialización de tipos polimórficos mediante JSON.
Colecciones
Use interfaces de colecciones de solo lectura en firmas de método RPC (por ejemplo, IReadOnlyList<T>) en lugar de tipos concretos (por ejemplo, List<T> o T[]
), lo que permite una deserialización potencialmente más eficaz.
Evite IEnumerable<T>.
Su falta de una Count
propiedad conduce a código ineficaz e implica la posible generación tardía de datos, que no se aplica en un escenario RPC.
Use IReadOnlyCollection<T> para colecciones desordenadas o IReadOnlyList<T> para colecciones ordenadas en su lugar.
Fíjese en IAsyncEnumerable<T>. Cualquier otro tipo de colección o IEnumerable<T> dará como resultado que toda la colección se envíe en un mensaje. El uso IAsyncEnumerable<T> de permite un mensaje inicial pequeño y proporciona al receptor los medios para obtener tantos elementos de la colección como desee, enumerarlo de forma asincrónica. Obtenga más información sobre este patrón de novelas.
Patrón de observador
Considere la posibilidad de usar el patrón de diseño de observador en la interfaz. Esta es una manera sencilla de que el cliente se suscriba a los datos sin los muchos problemas que se aplican al modelo de eventos tradicional descrito en la sección siguiente.
El patrón de observador puede ser tan sencillo como este:
Task<IDisposable> SubscribeAsync(IObserver<YourDataType> observer);
Los IDisposable tipos y IObserver<T> usados anteriormente son dos de los tipos exóticos de StreamJsonRpc, por lo que obtienen un comportamiento serializado especialmente en lugar de serializarse como datos simples.
Eventos
Los eventos pueden ser problemáticos sobre RPC por varias razones y se recomienda el patrón de observador descrito anteriormente en su lugar.
Tenga en cuenta que el servicio no tiene visibilidad sobre cuántos controladores de eventos ha asociado el cliente cuando el servicio y el cliente están en procesos independientes. JsonRpc siempre asociará exactamente un controlador responsable de propagar el evento al cliente. El cliente puede tener cero o más controladores asociados en el lado lejano.
La mayoría de los clientes RPC no tendrán controladores de eventos conectados cuando estén conectados por primera vez. Evite generar el primer evento hasta después de que el cliente haya invocado un método "Subscribe*" en la interfaz para indicar interés y preparación para recibir eventos.
Si el evento indica un delta en estado (por ejemplo, un nuevo elemento agregado a una colección), considere la posibilidad de generar todos los eventos anteriores o describir todos los datos actuales como si fuera nuevo en el argumento de evento cuando un cliente se suscriba para ayudarles a "sincronizarse" con nada más que el código de control de eventos.
Considere la posibilidad de aceptar argumentos adicionales en el método "Subscribe*" mencionado anteriormente si el cliente podría querer expresar interés en un subconjunto de datos o notificaciones, para reducir el tráfico de red y la CPU necesarios para reenviar estas notificaciones.
Considere la posibilidad de no ofrecer un método que devuelva el valor actual si también expone un evento para recibir notificaciones de cambio o desaconseja activamente que los clientes lo usen en combinación con el evento. Un cliente que se suscribe a un evento para los datos y llama a un método para obtener el valor actual es competir contra los cambios en ese valor y falta un evento de cambio o no saber cómo conciliar un evento de cambio en un subproceso con el valor obtenido en otro subproceso. Esta preocupación es general para cualquier interfaz, no solo cuando se a través de RPC.
Convenciones de nomenclatura
- Use el
Service
sufijo en interfaces RPC y un prefijo simpleI
. - No use el
Service
sufijo para las clases del SDK. La biblioteca o el contenedor RPC deben usar un nombre que describa exactamente lo que hace, evitando el término "servicio". - Evite el término "remoto" en los nombres de interfaz o miembros. Recuerde que los servicios asincrónicas se aplican de forma ideal tanto en escenarios locales como los remotos.
Problemas de compatibilidad de versiones
Queremos que cualquier servicio asincronado determinado que se exponga a otras extensiones o que se exponga a través de Live Share sea compatible con versiones anteriores y posteriores, lo que significa que un cliente puede ser más antiguo o más reciente que el servicio y que la funcionalidad debe ser aproximadamente igual a la de las dos versiones aplicables.
En primer lugar, revisemos la terminología de cambio importante:
Cambio importante binario: un cambio de API que provocaría que otro código administrado compilado con una versión anterior del ensamblado no se enlace en tiempo de ejecución al nuevo. Algunos ejemplos son:
- Cambiar la firma de un miembro público existente.
- Cambiar el nombre de un miembro público.
- Quitar un tipo público.
- Agregar un miembro abstracto a un tipo o cualquier miembro a una interfaz.
Pero los siguientes no son cambios importantes binarios:
- Agregar un miembro no abstracto a una clase o estructura.
- Agregar una implementación de interfaz completa (no abstracta) a un tipo existente.
Cambio importante en el protocolo: un cambio en la forma serializada de algún tipo de datos o llamada al método RPC para que la entidad remota no pueda deserializar y procesarla correctamente. Algunos ejemplos son:
- Adición de parámetros necesarios a un método RPC.
- Quitar un miembro de un tipo de datos que se garantizaba que no es NULL.
- Agregar un requisito de que se debe colocar una llamada de método antes de otras operaciones preexistentes.
- Agregar, quitar o cambiar un atributo en un campo o propiedad que controla el nombre serializado de los datos de ese miembro.
- (MessagePack): cambiar la DataMemberAttribute.Order propiedad o
KeyAttribute
el entero de un miembro existente.
Pero los siguientes no son cambios importantes en el protocolo:
- Agregar un miembro opcional a un tipo de datos.
- Agregar miembros a interfaces RPC.
- Agregar parámetros opcionales a los métodos existentes.
- Cambiar un tipo de parámetro que representa un entero o float a uno con mayor longitud o precisión (por ejemplo,
int
along
ofloat
adouble
). - Cambiar el nombre de un parámetro. Técnicamente, esto es importante para los clientes que usan argumentos con nombre JSON-RPC, pero los clientes que usan los ServiceJsonRpcDescriptor argumentos posicionales de uso de forma predeterminada y no se verían afectados por un cambio de nombre de parámetro. Esto no tiene nada que ver con si el código fuente del cliente usa la sintaxis de argumento con nombre, a la que un cambio de nombre de parámetro sería un cambio importante de origen.
Cambio importante en el comportamiento: un cambio en la implementación de un servicio asincrónica que agrega o cambia el comportamiento de modo que los clientes más antiguos no funcionen correctamente. Algunos ejemplos son:
- Ya no inicializa un miembro de un tipo de datos que se inicializó anteriormente.
- Se produce una excepción en una condición que anteriormente podía completarse correctamente.
- Devolver un error con un código de error diferente al que se devolvió anteriormente.
Pero los siguientes no son cambios importantes en el comportamiento:
- Iniciando un nuevo tipo de excepción (porque todas las excepciones se encapsulan de RemoteInvocationException todos modos).
Cuando se requieren cambios importantes, se pueden realizar de forma segura registrando y proferiendo un nuevo moniker de servicio. Este moniker puede compartir el mismo nombre, pero con un número de versión superior. La interfaz RPC original podría ser reutilizable si no hay ningún cambio importante binario. De lo contrario, defina una nueva interfaz para la nueva versión del servicio. Evite interrumpir los clientes antiguos al continuar registrando, proffer y admitiendo también la versión anterior.
Queremos evitar todos estos cambios importantes, excepto para agregar miembros a interfaces RPC.
Adición de miembros a interfaces RPC
No agregue miembros a una interfaz de devolución de llamada de cliente RPC, ya que muchos clientes pueden implementar esa interfaz y agregar miembros provocaría que CLR TypeLoadException iniciara cuando esos tipos se carguen, pero no implementen los nuevos miembros de la interfaz. Si debe agregar miembros para invocar en un destino de devolución de llamada de cliente RPC, defina una nueva interfaz (que puede derivar del original) y, a continuación, siga el proceso estándar para proferir el servicio asincrónico con un número de versión incrementado y ofrezca un descriptor con el tipo de interfaz de cliente actualizado especificado.
Puede agregar miembros a interfaces RPC que definan un servicio asincrónica. Este no es un cambio importante en el protocolo y es solo un cambio importante binario para los que implementan el servicio, pero probablemente actualizaría también el servicio para implementar el nuevo miembro. Dado que nuestra guía es que nadie debe implementar la interfaz RPC excepto el propio servicio asincrónica (y las pruebas deben usar marcos ficticios), agregar un miembro a una interfaz RPC no debe interrumpir a nadie.
Estos nuevos miembros deben tener comentarios de documento xml que identifiquen la versión del servicio que primero agregó ese miembro. Si un cliente más reciente llama al método en un servicio anterior que no implementa el método , ese cliente puede detectar RemoteMethodNotFoundException. Pero ese cliente puede predecir (y probablemente debería) predecir el error y evitar la llamada en primer lugar. Entre los procedimientos recomendados para agregar miembros a los servicios existentes se incluyen:
- Si este es el primer cambio dentro de una versión del servicio: aumente la versión secundaria en el moniker de servicio al agregar el miembro y declare el nuevo descriptor.
- Actualice el servicio para registrar y proffer la nueva versión además de la versión anterior.
- Si tiene un cliente del servicio asincrónica, actualice el cliente para solicitar la versión más reciente y la reserva para solicitar la versión anterior si la más reciente vuelve como null.