Publicado en

Azure Functions con C#: qué son y cómo funcionan

Cuando una API recibe tráfico de forma irregular, tener un servidor encendido las 24 horas es un gasto que no tiene sentido. El modelo serverless resuelve exactamente eso: el código se ejecuta solo cuando hay trabajo que hacer, y el proveedor cloud gestiona todo lo demás. Azure Functions es la implementación de ese modelo en el ecosistema de Microsoft.

Azure Functions con C# — modelo serverless en Azure

¿Qué es serverless?

Serverless no significa que no haya servidores. Los hay, pero tú no los ves ni los gestionas. No configuras máquinas virtuales, no instalas sistemas operativos, no defines reglas de escalado manual.

El modelo funciona así: tú escribes una función, la subes al proveedor cloud, y él se encarga de ejecutarla cuando llega un evento, escalarla si llega más carga, y apagarla cuando no hay trabajo. Pagas únicamente por el tiempo de ejecución real, no por infraestructura ociosa.

¿Qué es Azure Functions?

Azure Functions es el servicio de computación serverless de Microsoft Azure. Permite desplegar bloques de código en C#, JavaScript, Python, Java o TypeScript sin gestionar ningún servidor.

Cada función tiene dos partes fundamentales:

  • Un disparador (trigger): el evento que provoca su ejecución. Puede ser una petición HTTP, un mensaje en una cola, un temporizador o un archivo que llega al almacenamiento.
  • El código que se ejecuta: la lógica de negocio real.

Opcionalmente, una función puede incluir también bindings: conexiones declarativas a servicios externos que evitan tener que escribir código de conexión manual. Defines en la configuración que la función lee de una cola y escribe en una base de datos, y Azure gestiona la conexión por ti.

El modelo de ejecución

Cuando llega un evento que dispara tu función, Azure busca una instancia disponible para ejecutarla. Si la función lleva tiempo sin recibir tráfico y no hay ninguna instancia activa, el runtime necesita arrancar una nueva desde cero. Esto se llama cold start y añade latencia, normalmente entre 1 y 4 segundos dependiendo del lenguaje y el plan de hospedaje.

Una vez activa, las siguientes ejecuciones llegan a esa instancia caliente y responden mucho más rápido. Si el volumen crece, Azure levanta más instancias en paralelo de forma automática.

Hay dos planes principales:

  • Plan de consumo: pagas por ejecución. La función escala automáticamente. Existe riesgo de cold start. Es el modelo serverless puro.
  • Plan Premium: instancias precalentadas, sin cold start, con soporte para redes virtuales. Más caro, pero con latencia predecible.

Un punto importante: las funciones son stateless. No puedes guardar estado en memoria entre ejecuciones. Si necesitas persistir algo entre llamadas, tienes que usar almacenamiento externo: una base de datos, una caché como Redis o Azure Durable Functions.

Tipos de trigger

Azure Functions no es solo para APIs HTTP. Los disparadores más habituales son:

  • HTTP Trigger: la función responde a peticiones HTTP. El tipo más común para construir APIs y webhooks.
  • Timer Trigger: la función se ejecuta según un cron. Ideal para tareas programadas: limpiezas, informes, sincronizaciones.
  • Queue Trigger: se dispara cuando llega un mensaje a una Azure Storage Queue o a un Service Bus.
  • Blob Trigger: se ejecuta cuando se sube o modifica un archivo en Azure Blob Storage. Útil para procesar imágenes o documentos automáticamente.
  • Cosmos DB Trigger: se activa cuando hay cambios en un contenedor de Cosmos DB, usando el change feed.

Tu primera Azure Function en C#

Prerrequisitos

  • .NET 8 SDK
  • Azure Functions Core Tools (para ejecutar localmente)
  • Visual Studio 2022 o VS Code con la extensión de Azure Functions

Crear el proyecto

func init MiPrimeraFunction --worker-runtime dotnet-isolated --target-framework net8.0
cd MiPrimeraFunction
func new --name ObtenerProducto --template "HTTP trigger"

Esto genera la estructura del proyecto con un archivo ObtenerProducto.cs listo para modificar.

El código generado

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using System.Net;
 
public class ObtenerProducto
{
    private readonly ILogger<ObtenerProducto> _logger;
 
    public ObtenerProducto(ILogger<ObtenerProducto> logger)
    {
        _logger = logger;
    }
 
    [Function("ObtenerProducto")]
    public HttpResponseData Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req)
    {
        _logger.LogInformation("Petición recibida.");
 
        var response = req.CreateResponse(HttpStatusCode.OK);
        response.Headers.Add("Content-Type", "text/plain; charset=utf-8");
        response.WriteString("Función ejecutada correctamente.");
 
        return response;
    }
}

Lo importante del código:

  • [Function("ObtenerProducto")]: registra la función con ese nombre en el runtime de Azure.
  • [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")]: indica que responde a peticiones GET y POST sin requerir autenticación.
  • HttpRequestData: contiene toda la información de la petición entrante: cabeceras, cuerpo y parámetros de query.
  • HttpResponseData: la respuesta que construyes y devuelves.

Un ejemplo más cercano a producción

En un proyecto real querrás leer parámetros de ruta, devolver JSON y manejar errores. Este ejemplo muestra los patrones más habituales:

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using System.Net;
 
public class ProductosFunction
{
    private readonly ILogger<ProductosFunction> _logger;
 
    // En producción estos datos vendrían de una base de datos
    private static readonly List<Producto> _productos = new()
    {
        new Producto(1, "Teclado mecánico", 89.99m),
        new Producto(2, "Monitor 27\"", 299.99m),
        new Producto(3, "Ratón inalámbrico", 45.00m)
    };
 
    public ProductosFunction(ILogger<ProductosFunction> logger) => _logger = logger;
 
    [Function("ObtenerProductos")]
    public async Task<HttpResponseData> ObtenerTodos(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "productos")]
        HttpRequestData req)
    {
        _logger.LogInformation("Obteniendo lista de productos.");
        var response = req.CreateResponse(HttpStatusCode.OK);
        await response.WriteAsJsonAsync(_productos);
        return response;
    }
 
    [Function("ObtenerProductoPorId")]
    public async Task<HttpResponseData> ObtenerPorId(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "productos/{id:int}")]
        HttpRequestData req, int id)
    {
        var producto = _productos.FirstOrDefault(p => p.Id == id);
 
        if (producto is null)
        {
            var notFound = req.CreateResponse(HttpStatusCode.NotFound);
            await notFound.WriteAsJsonAsync(new { error = $"Producto con id {id} no encontrado." });
            return notFound;
        }
 
        var response = req.CreateResponse(HttpStatusCode.OK);
        await response.WriteAsJsonAsync(producto);
        return response;
    }
}
 
public record Producto(int Id, string Nombre, decimal Precio);

Tres cosas a destacar de este ejemplo:

  • Route = "productos/{id:int}": define rutas parametrizadas con restricción de tipo, igual que en una API REST convencional.
  • WriteAsJsonAsync: serializa el objeto a JSON y establece el Content-Type correcto automáticamente.
  • Los parámetros de ruta se inyectan directamente como parámetros del método, sin necesidad de parsearlos manualmente.

Ejecutar localmente

func start

La función estará disponible en http://localhost:7071/api/productos. Puedes probarla con curl, Postman o cualquier cliente HTTP.

Azure Functions vs ASP.NET Core: cuándo usar cada uno

Si ya sabes construir APIs con ASP.NET Core, la pregunta natural es cuándo tiene sentido usar Azure Functions en su lugar. No son tecnologías excluyentes, pero cada una encaja mejor en situaciones diferentes.

Usa Azure Functions cuando:

  • La carga es irregular o con picos impredecibles. El modelo de consumo te ahorra pagar por infraestructura ociosa.
  • El código es una tarea puntual: procesar un archivo, enviar notificaciones, ejecutar un job nocturno.
  • Necesitas integración nativa con otros servicios de Azure sin escribir código de conexión manual.
  • Estás construyendo un sistema orientado a eventos donde cada acción dispara la siguiente.

Usa ASP.NET Core cuando:

  • El tráfico es constante y predecible. Un servidor siempre activo resulta más barato que millones de invocaciones.
  • Necesitas control fino sobre el pipeline HTTP: middlewares, filtros, autenticación personalizada.
  • La latencia consistente es crítica y el cold start no es aceptable en tu caso de uso.

En proyectos reales es habitual encontrar ambos: una API principal en ASP.NET Core y funciones auxiliares en Azure Functions para las tareas asíncronas y de integración.

El modelo isolated worker

Si buscas documentación sobre Azure Functions en .NET encontrarás referencias tanto a Microsoft.NET.Sdk.Functions como a Microsoft.Azure.Functions.Worker. Son dos modelos distintos.

El modelo isolated worker —el que hemos usado en los ejemplos— ejecuta tu código en un proceso separado del host de Functions. Esto te da control total sobre la versión de .NET, compatibilidad con las versiones más recientes del framework y la posibilidad de usar inyección de dependencias y middleware exactamente igual que en ASP.NET Core.

El modelo antiguo in-process está en camino de deprecación. Para cualquier proyecto nuevo, usa siempre el modelo isolated worker con .NET 8.