ServiceStack: versiones RESTful de recursos

He leído el artículo Ventajas de los servicios web basados ​​en mensajes y me pregunto si existe un estilo / práctica recomendada para las versiones de Recursos relajantes en ServiceStack. Las diferentes versiones pueden dar respuestas diferentes o tener diferentes parámetros de entrada en el DTO de solicitud.

Me inclino por un control de versiones del tipo de URL (es decir, / v1 / movies / {Id}), pero he visto otras prácticas que establecen la versión en los encabezados HTTP (es decir, Content-Type: application / vnd.company.myapp-v2 )

Espero una forma que funcione con la página de metadatos, pero no tanto como lo he notado, simplemente usando la estructura de carpetas / espacios de nombres funciona bien cuando se renderizan rutas.

Por ejemplo (esto no se procesa correctamente en la página de metadatos, pero funciona correctamente si conoce la ruta / url directa)

  • / v1 / movies / {id}
  • /v1.1/movies/{id}

Código

namespace Samples.Movies.Operations.v1_1 { [Route("/v1.1/Movies", "GET")] public class Movies { ... } } namespace Samples.Movies.Operations.v1 { [Route("/v1/Movies", "GET")] public class Movies { ... } } 

y servicios correspondientes …

 public class MovieService: ServiceBase { protected override object Run(Samples.Movies.Operations.v1.Movies request) { ... } } public class MovieService: ServiceBase { protected override object Run(Samples.Movies.Operations.v1_1.Movies request) { ... } } 

Intentar evolucionar (no implementar) los servicios existentes

Para el control de versiones, tendrá un gran dolor si intenta mantener diferentes tipos estáticos para diferentes puntos finales de versión. Inicialmente comenzamos a seguir esta ruta, pero tan pronto como comience a admitir su primera versión, el esfuerzo de desarrollo para mantener múltiples versiones del mismo servicio explota ya que necesitará mantener un mapeo manual de diferentes tipos que se filtre fácilmente para tener que mantener múltiples implementaciones paralelas, cada una acoplada a un tipo de versiones diferentes: una violación masiva de DRY. Este es un problema menor para los lenguajes dynamics donde los mismos modelos pueden ser reutilizados fácilmente por diferentes versiones.

Aproveche las versiones incorporadas en los serializadores

Mi recomendación no es hacer una versión explícita, sino aprovechar las capacidades de control de versiones dentro de los formatos de serialización.

Por ejemplo: generalmente no necesita preocuparse por el control de versiones con clientes JSON ya que las capacidades de control de versiones de los Serializadores JSON y JSV son mucho más resistentes .

Mejore sus servicios existentes a la defensiva

Con XML y DataContract, puede agregar y eliminar campos libremente sin hacer un cambio radical. Si agrega IExtensibleDataObject a su DTO de respuesta, también tiene un potencial para acceder a los datos que no están definidos en el DTO. Mi enfoque para el control de versiones es progtwigr a la defensiva para no introducir un cambio de rotura, puede verificar que este sea el caso con las pruebas de integración que usan DTOs antiguos. Aquí hay algunos consejos que sigo:

  • Nunca cambie el tipo de una propiedad existente: si necesita que sea de otro tipo, agregue otra propiedad y use la anterior / existente para determinar la versión.
  • El progtwig realiza de forma defensiva qué propiedades no existen con los clientes más antiguos, por lo que no los hace obligatorios.
  • Mantenga un único espacio de nombre global (solo relevante para puntos finales XML / SOAP)

Lo hago utilizando el atributo [assembly] en AssemblyInfo.cs de cada uno de sus proyectos de DTO:

 [assembly: ContractNamespace("http://schemas.servicestack.net/types", ClrNamespace = "MyServiceModel.DtoTypes")] 

El atributo de ensamblaje le evita especificar manualmente espacios de nombres explícitos en cada DTO, es decir:

 namespace MyServiceModel.DtoTypes { [DataContract(Namespace="http://schemas.servicestack.net/types")] public class Foo { .. } } 

Si desea utilizar un espacio de nombres XML diferente al predeterminado anterior, debe registrarlo con:

 SetConfig(new EndpointHostConfig { WsdlServiceNamespace = "http://schemas.my.org/types" }); 

Incorporación de versiones en DTO

La mayoría de las veces, si progtwig de manera defensiva y desarrolla sus servicios con elegancia, no necesitará saber exactamente qué versión está usando un cliente específico, ya que puede deducirlo de los datos que se completan. Pero en los casos poco frecuentes en los que sus servicios necesitan modificar el comportamiento en función de la versión específica del cliente, puede incrustar información de versión en sus DTO.

Con la primera versión de tus DTO que publicas, puedes crearlas sin pensar en versiones.

 class Foo { string Name; } 

Pero tal vez por alguna razón el Formulario / UI se modificó y ya no deseaba que el Cliente usara la variable de Nombre ambiguo y también deseaba rastrear la versión específica que el cliente estaba usando:

 class Foo { Foo() { Version = 1; } int Version; string Name; string DisplayName; int Age; } 

Más tarde se discutió en una reunión del Equipo, DisplayName no era lo suficientemente bueno y debes dividirlos en diferentes campos:

 class Foo { Foo() { Version = 2; } int Version; string Name; string DisplayName; string FirstName; string LastName; DateTime? DateOfBirth; } 

Entonces, el estado actual es que tiene 3 versiones de cliente diferentes, con llamadas existentes que se ven así:

versión v1:

 client.Post(new Foo { Name = "Foo Bar" }); 

Versión v2:

 client.Post(new Foo { Name="Bar", DisplayName="Foo Bar", Age=18 }); 

versión v3:

 client.Post(new Foo { FirstName = "Foo", LastName = "Bar", DateOfBirth = new DateTime(1994, 01, 01) }); 

Puede seguir manejando estas versiones diferentes en la misma implementación (que usará la última versión de los DTO de v3), por ejemplo:

 class FooService : Service { public object Post(Foo request) { //v1: request.Version == 0 request.Name == "Foo" request.DisplayName == null request.Age = 0 request.DateOfBirth = null //v2: request.Version == 2 request.Name == null request.DisplayName == "Foo Bar" request.Age = 18 request.DateOfBirth = null //v3: request.Version == 3 request.Name == null request.DisplayName == null request.FirstName == "Foo" request.LastName == "Bar" request.Age = 0 request.DateOfBirth = new DateTime(1994, 01, 01) } } 

Enmarcando el problema

La API es la parte de tu sistema que expone su expresión. Define los conceptos y la semántica de la comunicación en su dominio. El problema surge cuando quieres cambiar lo que se puede express o cómo se puede express.

Puede haber diferencias tanto en el método de expresión como en lo que se expresa. El primer problema tiende a ser las diferencias en los tokens (nombre y apellido en lugar de nombre). El segundo problema es express cosas diferentes (la capacidad de renombrarse).

Una solución de control de versiones a largo plazo deberá resolver ambos desafíos.

Evolucionando una API

La evolución de un servicio cambiando los tipos de recursos es un tipo de control de versiones implícito. Utiliza la construcción del objeto para determinar el comportamiento. Funciona mejor cuando solo hay cambios menores en el método de expresión (como los nombres). No funciona bien para cambios más complejos en el método de expresión o cambios en el cambio de expresividad. El código tiende a dispersarse por todas partes.

Versiones específicas

Cuando los cambios se vuelven más complejos, es importante mantener la lógica para cada versión por separado. Incluso en el ejemplo de mythz, segregó el código para cada versión. Sin embargo, el código aún está mezclado en los mismos métodos. Es muy fácil para el código que las diferentes versiones comiencen a colapsarse entre sí y es probable que se extiendan. Deshacerse del soporte para una versión anterior puede ser difícil.

Además, deberá mantener su código anterior sincronizado con los cambios en sus dependencias. Si una base de datos cambia, el código que respalda el modelo anterior también deberá cambiar.

Una mejor manera

La mejor manera que he encontrado es abordar el problema de expresión directamente. Cada vez que se lanza una nueva versión de la API, se implementará sobre la nueva capa. Esto es generalmente fácil porque los cambios son pequeños.

Realmente brilla de dos maneras: primero todo el código para manejar el mapeo está en un lugar, por lo que es fácil de entender o eliminar más adelante y segundo, no requiere mantenimiento a medida que se desarrollan nuevas API (el modelo ruso de muñeca).

El problema es cuando la nueva API es menos expresiva que la antigua API. Este es un problema que deberá ser resuelto sin importar cuál sea la solución para mantener la versión anterior. Simplemente queda claro que hay un problema y cuál es la solución para ese problema.

El ejemplo del ejemplo de mythz en este estilo es:

 namespace APIv3 { class FooService : RestServiceBase { public object OnPost(Foo request) { var data = repository.getData() request.FirstName == data.firstName request.LastName == data.lastName request.DateOfBirth = data.dateOfBirth } } } namespace APIv2 { class FooService : RestServiceBase { public object OnPost(Foo request) { var v3Request = APIv3.FooService.OnPost(request) request.DisplayName == v3Request.FirstName + " " + v3Request.LastName request.Age = (new DateTime() - v3Request.DateOfBirth).years } } } namespace APIv1 { class FooService : RestServiceBase { public object OnPost(Foo request) { var v2Request = APIv2.FooService.OnPost(request) request.Name == v2Request.DisplayName } } } 

Cada objeto expuesto es claro. El mismo código de mapeo aún debe escribirse en ambos estilos, pero en el estilo separado, solo se debe escribir el mapeo relevante para un tipo. No es necesario mapear explícitamente el código que no se aplica (que es solo otra posible fuente de error). La dependencia de las API anteriores es estática cuando agrega API futuras o cambia la dependencia de la capa API. Por ejemplo, si la fuente de datos cambia, solo la API más reciente (versión 3) debe cambiar en este estilo. En el estilo combinado, necesitaría codificar los cambios para cada una de las API admitidas.

Una preocupación en los comentarios fue la adición de tipos a la base de códigos. Esto no es un problema porque estos tipos están expuestos externamente. Proporcionar los tipos de forma explícita en la base de código hace que sea fácil descubrirlos y aislarlos en las pruebas. Es mucho mejor que el mantenimiento sea claro. Otra ventaja es que este método no produce lógica adicional, sino que solo agrega tipos adicionales.

También estoy tratando de encontrar una solución para esto y estaba pensando en hacer algo como lo siguiente. (Basado en una gran cantidad de consultas de Googlling y StackOverflow por lo que esto se construye sobre los hombros de muchos otros).

Primero, no quiero debatir si la versión debe estar en el URI o en el Encabezado de solicitud. Existen ventajas y desventajas para ambos enfoques, por lo que creo que cada uno de nosotros debe usar lo que mejor se adapte a nuestros requisitos.

Se trata de cómo diseñar / architecture de los objetos de mensaje de Java y las clases de implementación de recursos.

Vamos a por ello.

Me acercaría a esto en dos pasos. Cambios menores (por ejemplo, 1.0 a 1.1) y cambios importantes (por ejemplo, 1.1 a 2.0)

Enfoque para cambios menores

Entonces digamos que vamos por las mismas clases de ejemplo usadas por @mythz

Inicialmente tenemos

 class Foo { string Name; } 

Brindamos acceso a este recurso como /V1.0/fooresource/{id}

En mi caso de uso, uso JAX-RS,

 @Path("/{versionid}/fooresource") public class FooResource { @GET @Path( "/{id}" ) public Foo getFoo (@PathParam("versionid") String versionid, (@PathParam("id") String fooId) { Foo foo = new Foo(); //setters, load data from persistence, handle business logic etc Return foo; } } 

Ahora digamos que agregamos 2 propiedades adicionales a Foo.

 class Foo { string Name; string DisplayName; int Age; } 

Lo que hago en este momento es anotar las propiedades con una anotación @Version

 class Foo { @Version(“V1.0")string Name; @Version(“V1.1")string DisplayName; @Version(“V1.1")int Age; } 

Luego tengo un filtro de respuesta basado en la versión solicitada, devuelva al usuario solo las propiedades que coinciden con esa versión. Tenga en cuenta que, por comodidad, si hay propiedades que deben devolverse para todas las versiones, entonces no las anote y el filtro lo devolverá independientemente de la versión solicitada.

Esto es como una capa de mediación. Lo que he explicado es una versión simplista y puede ser muy complicado, pero espero que entiendas la idea.

Enfoque para la versión principal

Ahora esto puede ser bastante complicado cuando se realizan muchos cambios de una versión a otra. Es entonces cuando tenemos que pasar a la segunda opción.

La opción 2 es esencialmente derivar la base de código y luego hacer los cambios en esa base de código y alojar ambas versiones en diferentes contextos. En este punto, podríamos tener que refactorizar un poco la base de código para eliminar la complejidad de mediación de la versión introducida en el Método uno (es decir, hacer que el código sea más limpio). Esto podría estar principalmente en los filtros.

Tenga en cuenta que esto es solo querer, estoy pensando y aún no lo he implementado y me pregunto si esta es una buena idea.

También me preguntaba si hay buenos motores de mediación / ESB que podrían hacer este tipo de transformación sin tener que usar filtros, pero no han visto ninguno que sea tan simple como usar un filtro. Tal vez no he buscado lo suficiente.

Interesado en conocer los pensamientos de los demás y si esta solución abordará la pregunta original.