Vanguardia

Servicios consultables

Dino Esposito

Dino EspositoSe está volviendo más habitual para las empresas exponer sus servicios empresariales de back-end como antiguos extremos HTTP sin formato. Este tipo de arquitectura no requiere ninguna información acerca de las bases de datos y los modelos de datos físicos. Las aplicaciones cliente ni siquiera necesitan referencias a bibliotecas específicas de bases de datos como Entity Framework. La ubicación física de los servicios es irrelevante y puede conservar el back-end local o moverlo de forma transparente a la nube. 

Como arquitecto de soluciones o desarrollador jefe, hay dos escenarios que debe estar preparado para afrontar al adoptar esta estrategia. El primero es cuando no tiene acceso alguno al funcionamiento interno de los servicios. En ese caso, no está ni siquiera en una condición de pedir más o menos datos para optimizar el rendimiento de la aplicación cliente.

El segundo escenario es cuando también es responsable de mantener esos servicios de back-end y puede influir en la API pública hasta cierto punto. En este artículo, me centraré principalmente en el último escenario. Hablaré del rol de las tecnologías específicas para implementar servicios consultables de una manera flexible. La tecnología que usaré es servicios de OData además de ASP.NET Web API. Casi todo lo descrito en este artículo se aplica además a las plataformas de ASP.NET existentes y a ASP.NET 5 vNext.

Servicios back-end sellados

Antes de entrar en el diseño de servicio consultable, explicaré brevemente ese primer escenario en el que no se tiene ningún control sobre los servicios disponibles. Se le asignan todos los detalles que necesita para realizar llamadas a dichos servicios, pero no tiene forma de modificar la cantidad y la forma de la respuesta.

Dichos servicios sellados están sellados por un motivo. Forman parte del back-end de TI oficial final de su empresa. Estos servicios forman parte de la arquitectura global y no se van a cambiar a la ligera. A medida que más aplicaciones cliente dependen de esos servicios, lo más probable es que su empresa considere el control de versiones. Sin embargo, por lo general, debe haber una razón de peso antes de implementar cualquier versión nueva de esos servicios.

Si la API sellada es un problema para la aplicación cliente que está desarrollando, lo único que puede hacer es incluir los servicios originales en una capa de proxy adicional. A continuación, puede utilizar cualquier truco que sirva para su propósito, incluido el almacenamiento en caché, nuevos agregados de datos y la inserción de datos adicionales. Desde un punto de vista arquitectónico, el conjunto de servicios resultante se desplaza del nivel de infraestructura al nivel de servicios de dominio. Incluso puede ser superior en el nivel de aplicación (vea la Figura 1).

De servicios sellados a servicios de aplicación más flexibles
Figura 1 De servicios sellados a servicios de aplicación más flexibles

El lado leído de una API

Las aplicaciones web modernas se crean en torno a una API interna. En algunos casos, esta API se hace pública. Es importante tener en cuenta que ASP.NET 5 vNext introduce una arquitectura en la que ASP.NET MVC y el motor de Razor proporcionan la infraestructura necesaria para generar vistas HTML.

ASP.NET Web API representa la infraestructura idónea para controlar las solicitudes de cliente procedentes de clientes que no sean exploradores y páginas HTML. En otras palabras, un nuevo sitio ASP.NET está diseñado idealmente como una delgada capa de HTML en torno a un conjunto posiblemente sellado de servicios de back-end. Sin embargo, el equipo responsable de la aplicación web, también es ahora el propietario de la API de back-end, en lugar del consumidor. Si alguien tiene un problema con eso, oirá sus quejas o sugerencias.

La mayoría de los problemas de la API de consumidor se refieren a la cantidad y calidad de los datos devueltos. El lado de la consulta de una API es normalmente el más difícil de crear porque nunca sabe de qué manera se están solicitando y usando sus datos a largo plazo. El lado de comandos de una API suele ser mucho más estable porque depende de los servicios y dominio empresariales. Los servicios de dominio cambian a veces, pero al menos mantienen un ritmo diferente y a menudo más lento.

Normalmente, tiene un modelo tras una API. El lado de la consulta de la API suele reflejar el modelo si la API tiene un tipo REST o RPC. En última instancia, el punto delicado de una API leída es el formato de los datos que devuelve y los agregados de datos que admite. Este problema tiene un nombre descriptivo: objetos de transferencia de datos (DTO).

Al crear un servicio de API, lo compila a partir de un modelo de datos existente y lo expone al mundo exterior a través del mismo modelo nativo o personalizado. Durante años, los arquitectos de software idearon aplicaciones de forma ascendente. Siempre empezaban desde la parte inferior de un modelo de datos típicamente relacional. Este modelo hacía todo el recorrido hasta el nivel de presentación.

En función de las necesidades de las distintas aplicaciones de cliente, algunas clases DTO se crearon a lo largo del camino para garantizar que la presentación pudiera tratar con los datos correctos en el formato adecuado. Este aspecto del desarrollo y de la arquitectura de software está cambiando en la actualidad bajo el efecto del rol cada vez más importante de las aplicaciones de cliente. A pesar de que crear el lado de comandos de una API de back-end sigue siendo una tarea relativamente sencilla, el diseño de un modelo de datos único y lo bastante general que se adapte a todos los posibles clientes es una tarea mucho más difícil. La flexibilidad de la API leída es un factor ganador en la actualidad porque nunca se sabe a qué aplicación cliente se tendrá que enfrentar alguna vez su API.

Servicios consultables

En la última edición del libro "Microsoft. NET: Architecting Applications for the Enterprise” (Microsoft Press, 2014) Andrea Saltarello y yo formalizamos un concepto que denominamos árboles de expresiones superpuestas (LET). La idea que subyace tras LET es que el nivel de aplicación y los servicios de dominio intercambian objetos IQueryable<T> siempre que sea posible. Esto suele ocurrir siempre que los niveles se encuentren en el mismo espacio de proceso y no requieren serialización. Al intercambiar IQueryable<T>, puede aplazar cualquier resultado de consulta requerido de la proyección de datos y la composición de filtros hasta el último minuto. Puede hacer que se adaten por aplicación, en lugar de alguna forma de código rígido de la API de nivel de dominio.

La idea de LET va de la mano con el patrón de CQRS emergente. Esto introduce la separación de la pila leída de la pila de comandos. En la Figura 2 se muestran los puntos de tener un patrón LET y una serie de servicios consultables en la arquitectura.

Los objetos consultables se consultan en el último minuto
Figura 2 Los objetos consultables se consultan en el último minuto

La principal ventaja de LET es que no es necesario ningún DTO para llevar datos entre niveles. Todavía necesitará tener clases de modelos de vistas en algún momento, pero esa es otra historia. Tiene que tratar con clases de modelos de vistas, siempre que tenga una interfaz de usuario para rellenar con datos. Las clases de modelos de vistas expresan el diseño de datos deseado que los usuarios esperan. Ese es el único conjunto de clases DTO que va a tener. Todo lo que no esté en el nivel en el que consulta físicamente los datos y en niveles superiores se proporciona mediante referencias IQueryable.

Otra ventaja de LET y los servicios consultables es que las consultas resultantes son consultas de nivel de aplicación. Su lógica sigue estrechamente lo relacionado con el lenguaje de expertos de dominio. Esto facilita la asignación de requisitos para código y el análisis de supuestos errores o malentendidos con los clientes. La mayoría del tiempo, un vistazo rápido al código ayuda a explicar la lógica. Por ejemplo, este es el aspecto que puede tener una consulta LET:

var model = from i in db.Invoices
                        .ForBusinessUnit(buId)
                        .UnpaidInLast(30.Days)
  orderby i.PaymentDueDate
  select new UnpaidViewModel
    {
      ...
    };

Desde el contexto de la base de datos de un objeto raíz de Entity Framework, consulta todas las facturas entrantes y selecciona las relevantes para una unidad empresarial determinada. Entre ellas, encontrará las que siguen sin pagarse varios días después de los plazos de vencimiento.

Lo bueno es que las referencias de IQueryable no son datos reales. La consulta se ejecuta contra el origen de datos solo cuando intercambia IQueryable por alguna IList. Los filtros que agrega por el camino son cláusulas WHERE agregadas a la consulta real que se está ejecutando en algún momento. Siempre y cuando esté en el mismo espacio de proceso, la cantidad de datos transferidos y almacenados en memoria es la indispensable.

¿Cómo afecta esto a la escalabilidad? La tendencia emergente para la que se ha optimizado la plataforma vNext es mantener el back-end web lo más compacto posible. Idealmente, será un solo nivel. La escalabilidad se logra replicando el nivel único a través de una variedad de roles web de Microsoft Azure. Tener un nivel único para el back-end web le permite usar IQueryable en todas partes y ahorrarse varias clases DTO.

Implementar servicios consultables

En el fragmento de código anterior, he supuesto que sus servicios se implementan como un nivel en torno a algún contexto de base de datos de Entity Framework. Sin embargo, ese es solo un ejemplo. También puede encapsular por completo el proveedor de datos reales bajo una fachada de ASP.NET Web API. De esa manera, tendrá la ventaja de una API que expresa las capacidades de servicios de dominio y que todavía puede llegar a HTTP, desacoplando así clientes desde una tecnología y plataforma específicas.

A continuación, puede crear una biblioteca de clases Web API y hospedarla en algún sitio ASP.NET MVC, servicio de Windows o incluso alguna aplicación host personalizada. En el proyecto Web API, el usuario crea clases de controlador que se derivan de ApiController y expone métodos que devuelven IQueryable<T>. Por último, decora cada método IQueryable con el atributo EnableQuery. También funciona el atributo Queryable, ahora obsoleto. El factor clave aquí es que el atributo EnableQuery le permite anexar las consultas de OData a la dirección URL solicitada, algo parecido a esto:

[EnableQuery]
public IQueryable<Customer> Get()
{
  return (from c in db.Customers select c);
}

Este fragmento básico de código representa el corazón de la API. No devuelve ningún dato por sí mismo. Permite a los clientes dar forma a los datos devueltos que desean. Examine el código de la Figura 3 y considérelo código de una aplicación cliente.

Figura 3 Las aplicaciones cliente pueden forma a los datos devueltos

var api = "api/customers?$select=LastName";
var request = new HttpRequestMessage()
{
  RequestUri = new Uri(api)),
    Method = HttpMethod.Get,
};
var client = new HttpClient();
var response = client.SendAsync(request).Result;
if (response.IsSuccessStatusCode)
{
  var list = await
    response.Content.ReadAsAsync<IEnumerable<Customer>>();
  // Build view model object here
}

La convención $select de la dirección URL determina la proyección de datos que recibe el cliente. Puede utilizar la eficacia de la sintaxis de consulta de OData para dar forma a la consulta. Para obtener más información acerca de esto, vea bit.ly/15PVBXv.

Por ejemplo, un cliente solo puede solicitar un pequeño subconjunto de columnas. Otro cliente o una pantalla diferente en el mismo cliente puede consultar un fragmento mayor de datos. Todo esto puede ocurrir sin tener que tocar la API ni crear toneladas de DTO por el camino. Todo lo que necesita es un servicio Web API consultable de OData y las clases de modelos de vistas finales que se van a consumir. Los datos transferidos se reducen al mínimo, ya que solo se devuelven campos filtrados.

Hay un par de aspectos destacables de este asunto. En primer lugar, OData es un protocolo detallado y de otra manera no se le podría dar el papel que desempeña. Esto significa que cuando se aplica una proyección de $select, la carga de JSON todavía enumerará todos los campos del IQueryable original<T> (todos los campos de la clase Customer original del método Get). Sin embargo, solo los campos especificados contendrán un valor.

Otro punto a tener en cuenta es la distinción entre mayúsculas y minúsculas. Esto afecta el elemento de consulta $filter que usa para agregar una cláusula WHERE a la consulta. Puede que desee llamar a las funciones tolower o toupper de OData (si las admite la biblioteca de cliente de OData que está usando) para normalizar las comparaciones entre cadenas.

Resumen

Francamente, nunca creí que OData mereciera ser tomada seriamente en consideración hasta que me encontré, como propietario de la API de back-end, en medio de una tormenta de solicitudes para diferentes DTO que se estaban devolviendo del mismo modelo de datos. Todas las solicitudes parecían legítimas y realizadas por una causa muy noble: la mejora del rendimiento.

En algún momento, parecía que lo que querían hacer todos esos clientes era "consultar" el back-end de manera muy similar a como consultarían una tabla de base de datos normal. Entonces, actualicé el servicio back-end para exponer los extremos OData, ofreciendo así a cada cliente la flexibilidad de descargar únicamente los campos en los que estaba interesado.

El tipo T de cada método IQueryable es la clave. Puede o no que sea el mismo T que tiene en el modelo físico. Puede coincidir con una tabla de base de datos sin formato o ser el resultado de agregaciones de datos realizadas en el servidor y transparente para los clientes. No obstante, al aplicar OData, permite a los clientes consultar conjuntos de datos de una única entidad conocida, T, así que ¿por qué no probarlo?


Dino Esposito es el coautor de “Microsoft .NET: Architecting Applications for the Enterprise” (Microsoft Press, 2014) y “Programming ASP.NET MVC 5” (Microsoft Press, 2014). Esposito, evangelizador técnico para las plataformas Android y Microsoft .NET Framework en JetBrains y orador frecuente en eventos del sector en todo el mundo, comparte su visión de software en software2cents.wordpress.com y en Twitter en twitter.com/despos.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Jon Arne Saeteras