Por mi experiencia he podido observar que muchos developers no aplican o desconocen el concepto de binding o enlace. En este artículo vamos a conocerlos un poco mejor 😉
Pocos desarrolladores a estas alturas no habrán oído hablar del concepto «serverless» o de las arquitecturas sin servidor. En Azure para poder implementarlas disponemos del servicio de Azure Functions.
Si ya tienes algunas horas de vuelo en este tema, seguro que conoces el concepto de trigger. Estos triggers o desencadenadores provocan la ejecución de una Azure Function cuando sucede algo, como por ejemplo, cuando se escribe un mensaje en un Service Bus. Pero, como decía al principio, tras muchos años de experiencia he observado que algunos desarrolladores no aplican o no conocen el concepto de binding o enlace.
Quizá sí se conoce el concepto de enlace de entrada, ya que el trigger, en sí mismo, es un tipo especial de enlace de entrada que permite leer del origen de datos que ha disparado la ejecución de la función, pero sin duda, el tipo de enlace que menos se emplea es el de salida. Por ejemplo, si en una Azure Function queremos escribir un mensaje en una cola o insertar datos en un Table Storage, la mayoría de desarrolladores hacen uso del SDK de Azure Storage correspondiente y generan una solución «a mano», obviando la existencia de esta característica que nos simplifica el trabajo, con algún matiz a tener en cuenta.
¿Qué son los bindings o enlaces?
Los enlaces o bindings conectan recursos o datos a una función. Podemos tener varios enlaces para acceder a diferentes tipos de datos, lo que nos permite conectarnos al origen o al destino del dato sin tener que escribir la lógica específica para ello. Hay dos tipos de enlaces:
- Entrada: Permiten leer datos. Se conecta al origen de datos.
- Salida: Permiten escribir datos. Se conecta al destino de los datos.
Hay bastantes más enlaces de salida que de entrada (todos los tipos de funciones menos las de tipo temporizador tienen enlaces de salida). Los enlaces nos van a permitir establecer un flujo de ejecución de funciones haciendo uso de una característica nativa del servicio, sin necesidad de escribir más código extra.
Vamos al lío, y veremos qué podemos hacer con estos enlaces.
Show me the code
En este ejemplo se va a enlazar la ejecución de varias Azure Functions haciendo uso de los enlaces de salida. En primer lugar, esta demo se ha realizado en .NET 6 y Visual Studio 2022, que nos gusta estar a la última. Por tanto, con la versión 4.x de Azure Functions. Podemos crear el proyecto de tipo Azure Functions con el wizard de Visual Studio, en VsCode o desde un terminal haciendo uso de los comandos de Azure Functions Core Tools.
En este caso hemos escrito el ejemplo en C#, pero podríamos hacer uso de cualquier otro lenguaje de programación como JavaScript o Python. En C#, para hacer uso de las extensiones del SDK de Azure Storage empleadas, hay que instalar el paquete Nuget Microsoft.Azure.WebJobs.Extensions.Storage, en este caso hemos usado la versión 4.0.5 para poder hacer uso de los atributos de los enlaces de salida de Table Storage, ya que la versión 5.0.0 de la librería no hace uso de los mismos (más info).
Como decíamos, el objetivo de este ejemplo es conectar varias Azure Function mediante enlaces de salida. Para ello, vamos a tener tres functions:
- Un HttpTrigger que recibe unos datos en el cuerpo de la petición y los escribe en un mensaje en un Azure Queue Storage.
- Un QueueTrigger que va a copiar el mensaje en un fichero JSON y lo almacena en un Blob Storage.
- Un BlobTrigger que lee el contenido del fichero e inserta un registro en un Azure Table Storage.
Azure Function HTTP Trigger.
Creamos un endpoint HTTP muy sencillo al que, mediante un POST, se le envían los datos de un grupo de música. Como vemos, el método Run tiene este atributo:
[return: Queue("%BandQueueName%")]
Esta línea indica, mediante un enlace de salida de Azure Queue Storage, que el tipo de dato que devuelve la Azure Function, un array de bytes, se va a escribir en la cola de mensajes que indiquemos en el parámetro. En caso de que no existiese la cola, se crearía automáticamente en el Storage correspondiente, en este ejemplo, en el emulador.
public static class BandApi { [FunctionName("BandApi")] [return: Queue("%BandQueueName%")] public static async Task<byte[]> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "band")] HttpRequest req) { using MemoryStream ms = new(); await req.Body.CopyToAsync(ms); return ms.ToArray(); } }
Azure Function Queue Trigger.
Esta Azure Function recibe el mensaje y esta vez, en lugar de usar un atributo en el método, se usa como parámetro de salida de la función, indicando que se va a escribir en un blob de un contenedor determinado, generando un fichero JSON cuyo nombre será un GUID. Como anteriormente, si no existiese el contenedor dónde queremos almacenar el fichero se crearía automáticamente.
public static class BandMessageHandler { [FunctionName("BandMessageHandler")] public static async Task Run( [QueueTrigger("%BandQueueName%")] byte[] message, [Blob("%BandsContainerName%/{rand-guid}.json", FileAccess.Write)] CloudBlockBlob outputBlob) { using var ms = new MemoryStream(message); await outputBlob.UploadFromStreamAsync(ms); } }
Azure Function Blob Trigger
Para terminar, se recibe el contenido del fichero y se inserta en un Table Storage. Haciendo uso de nuevo del atributo en el método, de esta manera:
[return: Table("%BandTable%")]
En este caso, si la tabla no existiese, obtendríamos un error, por lo que habría que crearla previamente. La función quedaría así:
public static class StoreBand { [FunctionName("StoreBand")] [return: Table("%BandTable%")] public static async Task<BandEntity> Run( [BlobTrigger("%BandsContainerName%/{name}")] Stream myBlob, string name) { var band = await JsonSerializer.DeserializeAsync<Band>(myBlob, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return new BandEntity { PartitionKey = band.Genre, RowKey = Guid.NewGuid().ToString(), Name = band.Name, FoundationYear = band.FoundationYear, Albums = band.Albums }; } }
Podéis ver la demo del ejemplo en el siguiente vídeo:
Conclusión.
Los enlaces de salida nos permiten escribir en el destino de datos de manera nativa, sin la necesidad de un trabajo extra por parte del desarrollador para realizar esta tarea. A su vez, nos va a permitir separar responsabilidades en nuestras funciones, pudiendo generar un flujo de ejecución lógico en función de nuestras necesidades.
Adjunto un enlace al repo de Github con el código.
Saludos y Happy Coding!