DynamicTableEntity: Azure Storage con Entidades Dinámicas

Recuerdo por allá en la prehistoria de Azure cuando pocos éramos los que nos aventurábamos en esas aguas inexploradas, que cuando uno quería usar el Azure Storage y en especial las tablas, tenía que generar toda una capa de acceso a datos con la parafernalia que ello conlleva para poder usar el api que en esa época se llamaba Microsoft.WindowsAzure.StorageClient.dll. Hasta la versión 1.7 se llegó con este acercamiento. A uno le tocaba SIEMPRE crear clases correlativas a las tablas que estaban en el storage y además acoplarlas a la librería lo que hacía difícil las pruebas unitarias. Adicionalmente había que crear los wrappers para hacer operaciones CRUD.

Luego vino el revolcón de la versión 2.0 del storage API que se identifica con el cambio de nombre a Microsoft.WindowsAzure.Storage.dll y después de esto, el fuerte encaminamiento que tomamos en Microsoft para ir a toda velocidad alcanzando los estándares y tendencias de la industria, obviamente con un trabajo impecable. Es así como hoy en día con el dominio del dinamismo y temas como nodejs y demás, ya vamos en la version 4 del api. Mantenemos el nombre desde la versión 2 porque los cambios no han sido tan “breaking”. Pero sí han traído cosas maravillosas como las DynamicTableEntity.

Gracias a ellas, podemos trabajar con las Azure Tables desde el api para .net sin necesidad de mapearlas al estilo ORM como antaño era requerido. Solo basta entender un poco las abstracciones que hemos hecho sobre el storage, para hacer los llamados correctos.

Es mi intención entonces en este post, mostrar la manera AGIL y DINAMICA de acceder al storage, en este caso, desde una sencilla aplicación de escritorio.

Lo primero que hay que hacer es importar el api. Agrega una referencia a Microsoft.WindowsAzure.Storage.dll. Ojo; no uses Microsoft.WindowsAzure.StorageClient.dll porque es la versión antigua que no contiene la magia que voy a mostrar ahora.

Del api, vamos a necesitar puntualmente dos Namespaces:

 using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Table;

Acto seguido, creamos la conexión. Lo haremos “manualmente” sin necesidad de agregar más pasos por ejemplo a través del uso del CloudConfigurationManager, para mantenerlo todo más simple. Si vamos a usar el DevelopmentStorage, en este post muestro cómo hacer lo mismo.

 CloudStorageAccount _account = CloudStorageAccount.Parse(
   String.Format("DefaultEndpointsProtocol=https;AccountName={0};"
       "AccountKey={0}",
       "tucuenta",
        "45JHDAMEegL7G7wlCZhZ60UYNltbj6bZiH9x3eLHNSD1TEIQ+jw=="));

Luego de esto, procedemos a crear un cliente de tablas. Este cliente, nos permitirá hacer todas las operaciones que requiramos con ella. Así hay clientes para blobs y colas. Anteriormente, todo esto quedaba dentro del mismo Namespace. Hoy en día, como vemos, tenemos un Namespace separado para cada elemento del storage de Azure. Aquí hemos incluido obviamente el Namespace de tablas.

 CloudTableClient _tableClient = _account.CreateCloudTableClient();

Ahora supondremos que estamos trabajando sobre una cuenta de storage que ya tiene data probablemente obtenida a través de otra app y que lo que haremos es consultar dicha data. Usando Cloud Storage Studio:

image

Así pues, debemos obtener una referencia a esa tabla para poder trabajar con ella. Dicha referencia se obtiene a través del cliente que creamos:

 CloudTable _tblInventory = _tableClient.GetTableReference("Inventory");

Ahora; cómo consultar dicha tabla sin tener una clase/entidad que la represente dentro de nuestro proyecto?

Hay varios tipos de consultas sobre el storage de Azure. Podemos por ejemplo traer todos los elementos dentro de una partición dada, solo un subconjunto de elementos dentro de una partición, o solo un elemento obviamente especificando la PartitionKey y la RowKey. En este artículo encuentran una descripción de todas las operaciones que se pueden ejecutar sobre el storage. Solo que allí no se muestra cómo se puede operar sin necesidad de crear las entidades de mapeo. Y ese precisamente es el objeto de este post. Así que todas las operaciones indicadas allí, se pueden hacer dinámicamente, si siguen la metodología que describo aquí.

Entonces para nuestra ilustración requeriremos hacer una consulta de todos los laptops que vende nuestra ficticia tienda de tecnología en la que en la tabla inventario tenemos como PartitionKey la categoría del elemento de tecnología, por ejemplo: compute, mobile, videogames, etc. En el RowKey tendríamos entonces la referencia de cada producto y además tendríamos entre muchas otras propiedades, la SubCategory del producto; por ejemplo, en la categoría cómputo, tendríamos subcategorías como laptop, PC, server, etc. Estamos interesados en laptops.

Así que lo que queremos es seleccionar todos los elementos cuya PartitionKey=”Compute” y cuya Subcategory=”laptop”.

Iniciemos entonces armando los filtros usando el método estático GenerateFilterCondition de TableQuery que trabaja así:

(valorIzquierdo, operadorComparativo, valorDerecho):

 string categoryFilter = TableQuery.GenerateFilterCondition(
    "PartitionKey",
    QueryComparisons.Equal,
    "Compute");

string subcategoryFilter = TableQuery.GenerateFilterCondition(
    "SubCategory",
    QueryComparisons.Equal,
    "Laptop");

Ahora debemos combinar estos filtros con un “AND”, de acuerdo a nuestros requerimientos. Para esto usamos el método estático CombineFilters de la clase TableQuery que recibe dos filtros y un operador lógico.

(Filtro1, Operador, Filtro2):

  string combinedFilters = TableQuery.CombineFilters(
     categoryFilter,
     TableOperators.And,
     subcategoryFilter);

Como se puede ver, todo esto es mero texto. Pero usando estos metoditos nos ahorramos posibles errores al manipular mucho texto, como cuando escribíamos comandos de “T-SQL” para ejecutar por allá en los tiempos del ADO.NET. De hecho, el filtro resultante es:

(PartitionKey eq 'Compute') and (SubCategory eq 'Laptop')

Que si ponemos en una herramienta como Cloud Storage Studio nos da:

image

Todo esto porque nos basamos en el estándar de WCF Data Services Filters.

Ahora usamos todo eso para armar una instancia de TableQuery que luego una tabla (tblInventory) ejecutará, obteniendo un conjunto IEnumerable de precisamente DynamicTableEntity, donde cada elemento es un registro coincidente con la consulta:

 TableQuery query = new TableQuery().Where(combinedFilters);
IEnumerable<DynamicTableEntity> virtualResults = 
    _tblInventory.ExecuteQuery(query);

Hasta aquí solo tenemos la definición del conjunto de datos que queremos y esto es Lazy; o sea que aún no se ha ejecutado contra el servidor, pues esto solo sucede cuando en realidad iteremos sobre los elementos. Por ejemplo si convertimos esto a una lista de DynamicTableEntity:

 List<DynamicTableEntity> laptops = virtualResults.ToList();

Ahora supongamos que queremos mostrar en la consola las referencias de cada uno de los elementos filtrados (laptops) y la existencia actual. Entonces sencillamente podríamos escribir:

 foreach(var laptop in laptops)
{
    Console.WriteLine(
        String.Concat(
            laptop.RowKey,
            "\t",
            laptop["StockAmount"].Int32Value));
}

Obsérvese que las propiedades nativas de las tablas de Azure como PartitionKey, RowKey, ETag, y Timestamp vienen fuertemente tipadas en una DynamicTableEntity, mientras que a las demás debemos acceder a manera de diccionario, además teniendo la facilidad de elegir el tipo de dato que vamos a recibir.

Al final obtenemos:

image

De esta manera tendríamos cubierta la consulta de elementos tablas de Azure de manera dinámica.

Ahora; cómo se haría una inserción o un update?

Para explicar ambas cosas usaré la operación InsertOrReplace que ejecutará nuestra tabla. En este caso, primero necesitaré ejecutar la operación Retrieve para ver si el elemento que busco ya existe y modificarlo, o si no, crear uno nuevo.

 TableOperation retrieve = TableOperation.Retrieve("Compute", "X220");
TableResult retrievedLaptop = _tblInventory.Execute(retrieve);
//Tenemos que hacer un casting a DynamicEntity para trabajar ágilmente
//El resultado originalmente viene en un object
DynamicTableEntity dynaLaptop = 
   (DynamicTableEntity)retrievedLaptop.Result;

Por ejemplo, llegó un embarque de 100 laptops de referencia X220 y hemos de agregarlos al inventario. Ya existirán? En ese caso debemos hacer un update. Si no, será una inserción nueva.

Veamos; si la entidad no existe, entonces creamos una nueva:

 if(dynaLaptop==null)
{
    dynaLaptop = new DynamicTableEntity()
    {
        PartitionKey = "Compute",
        RowKey = "Laptop"
    };
}

Solo las propiedades nativas de las tablas de Azure como PartitionKey, RowKey, ETag, y Timestamp vienen por defecto en una DynamicTableEntity. Las otras como “stockAmount” debemos incluirlas de la siguiente manera:

 dynaLaptop.Properties.Add("StockAmount", 
    EntityProperty.GeneratePropertyForInt(100));
dynaLaptop.Properties.Add("SubCategory", 
    EntityProperty.GeneratePropertyForString("Laptop"));
/*No es necesario crear propiedades para todos los campos
 existentes en la tabla. Aquí intencionalmente dejé por fuera 
 ModelName y UnitPrice. Además dada la flexibilidad del
 Azure Storage, podemos agregar propiedades que no estaban 
 antes en la tabla:*/
dynaLaptop.Properties.Add("HDD", 
   EntityProperty.GeneratePropertyForString("300GB"));

Si por el contrario, la entidad ya existía, lo que hacemos es actualizarla:

 dynaLaptop["StockAmount"].Int32Value += 100;                
//Aquí también podemos aregar nuevas propiedades
dynaLaptop.Properties.Add("RAM", 
    EntityProperty.GeneratePropertyForString("32GB"));

Así que ahora que ya tenemos la entidad lista para enviarla al storage usamos:

 TableOperation updateOperation = 
    TableOperation.InsertOrReplace(dynaLaptop);
_tblInventory.Execute(updateOperation);

Si luego de esto chequeamos la tabla en la nube tenemos:

image

Donde se aprecia que se insertó el nuevo elemento con la nueva propiedad HDD. Y que no pasó nada, por no haber indicado por ejemplo un ModelName.

Ahora, si volvemos a ejecutar, lo que tendremos es un update, donde se sumarán otras 100 unidades y se agregará la propiedad RAM:

image

Y eso sería todo. La solución completa la pueden encontrar en GitHub:

Hemos visto entonces cómo podemos acceder al Azure storage de una manera ágil y dinámica, sin necesidad de hacer montones de preparativos código circenses para lograrlo. Al mejor estilo nodejs ;) Algo muy útil cuando ya tenemos datos en la nube y queremos hacer un sencillo cliente para usarlos, sin necesidad de montar toda una arquitectura de mapeo detrás; cosa que obviamente aún sigue estando disponible para los desarrollos que lo justifiquen.