5.4 Plugins con Azure Functions

Extiende plugins con Azure Functions y Service Bus

Los plugins tienen limitaciones inherentes: timeouts cortos, sandbox restrictivo, sin soporte para async/await nativo. Para escenarios que superan estas limitaciones, Azure Functions ofrece una salida elegante. Un plugin puede disparar una Function que maneja operaciones de larga duración, integraciones complejas, o procesamiento que requiere recursos externos.

Objetivos de aprendizaje

  • Entender cuándo combinar plugins con Azure Functions
  • Implementar el patrón de plugin que llama a Function
  • Usar Service Bus para comunicación desacoplada
  • Conocer los webhooks nativos de Dataverse

¿Por qué combinar plugins con Azure Functions?

Cada tecnología tiene sus fortalezas. Los plugins están perfectamente integrados con Dataverse: acceso al contexto, transacciones, seguridad. Pero tienen limitaciones que a veces no puedes superar.

Las Azure Functions complementan donde los plugins se quedan cortos:

Timeout extendido: Un plugin síncrono tiene 2 minutos. Una Azure Function puede ejecutarse por horas si es necesario.

Sin restricciones sandbox: Azure Functions tienen acceso completo a librerías, certificados, puertos no estándar, todo lo que necesites.

Async nativo: Puedes usar async/await correctamente, pattern matching avanzado, cualquier característica moderna de C#.

Mejor observabilidad: Application Insights te da logs estructurados, métricas, y trazas sin configuración adicional.

Gestión de secretos: Azure Key Vault se integra nativamente. Managed Identity evita credenciales en código.


Patrón: Plugin dispara Azure Function

El patrón más común es un plugin que hace una llamada HTTP a una Azure Function:


public class SincronizarConERPPlugin : IPlugin
{
    private const string FunctionUrl = 
        "https://mi-function-app.azurewebsites.net/api/SincronizarCuenta";
    private const string FunctionKey = "tu-function-key-del-portal";
    
    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)
            serviceProvider.GetService(typeof(IPluginExecutionContext));
        var trace = (ITracingService)
            serviceProvider.GetService(typeof(ITracingService));
        
        Entity target = (Entity)context.InputParameters["Target"];
        
        // Preparar datos para enviar a la Function
        var payload = new
        {
            accountId = target.Id.ToString(),
            name = target.GetAttributeValue("name"),
            telephone = target.GetAttributeValue("telephone1"),
            modifiedOn = DateTime.UtcNow
        };
        
        string json = JsonConvert.SerializeObject(payload);
        trace.Trace($"Enviando a Function: {json}");
        
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
        
        using (HttpClient client = new HttpClient())
        {
            client.Timeout = TimeSpan.FromSeconds(30);
            
            // Function Key para autenticación
            client.DefaultRequestHeaders.Add("x-functions-key", FunctionKey);
            
            var content = new StringContent(json, Encoding.UTF8, "application/json");
            
            HttpResponseMessage response = client.PostAsync(FunctionUrl, content).Result;
            
            trace.Trace($"Respuesta de Function: {response.StatusCode}");
            
            if (!response.IsSuccessStatusCode)
            {
                string error = response.Content.ReadAsStringAsync().Result;
                throw new InvalidPluginExecutionException(
                    $"Error sincronizando con ERP: {error}");
            }
        }
        
        trace.Trace("Sincronización completada");
    }
}

La Azure Function correspondiente


[FunctionName("SincronizarCuenta")]
public async Task Run(
    [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
    ILogger log)
{
    log.LogInformation("Recibida solicitud de sincronización");
    
    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    dynamic data = JsonConvert.DeserializeObject(requestBody);
    
    string accountId = data.accountId;
    string name = data.name;
    
    log.LogInformation($"Sincronizando cuenta {accountId}: {name}");
    
    try
    {
        // Aquí puedes hacer operaciones de larga duración:
        // - Llamar al ERP con reintentos complejos
        // - Procesar archivos
        // - Llamar a múltiples servicios
        await SincronizarConERP(accountId, name);
        
        log.LogInformation("Sincronización exitosa");
        return new OkObjectResult(new { status = "success" });
    }
    catch (Exception ex)
    {
        log.LogError(ex, "Error en sincronización");
        return new StatusCodeResult(500);
    }
}

Patrón: Fire and Forget con Azure Service Bus

El patrón anterior es síncrono: el plugin espera la respuesta de la Function. Pero a veces no necesitas esperar. Para estos casos, Service Bus desacopla completamente al plugin del procesamiento.


// En el plugin: enviar mensaje a la cola
using Azure.Messaging.ServiceBus;

public void EnviarAServiceBus(object datos, ITracingService trace)
{
    string connectionString = "tu-connection-string-de-service-bus";
    string queueName = "dataverse-events";
    
    string json = JsonConvert.SerializeObject(datos);
    
    ServiceBusClient client = new ServiceBusClient(connectionString);
    ServiceBusSender sender = client.CreateSender(queueName);
    
    ServiceBusMessage message = new ServiceBusMessage(json);
    
    sender.SendMessageAsync(message).Wait();
    
    trace.Trace($"Mensaje enviado a cola: {queueName}");
}

Una Azure Function con trigger de Service Bus procesa los mensajes:


[FunctionName("ProcesarEventoDataverse")]
public async Task Run(
    [ServiceBusTrigger("dataverse-events")] string messageBody,
    ILogger log)
{
    log.LogInformation($"Procesando mensaje: {messageBody}");
    
    // Procesar sin restricciones de tiempo
    await ProcesarEvento(messageBody);
}

Ventajas de este patrón:

  • El plugin termina inmediatamente después de enviar el mensaje
  • Service Bus garantiza entrega (el mensaje no se pierde)
  • Reintentos automáticos si la Function falla
  • Escalado independiente: puedes procesar miles de mensajes en paralelo

Webhooks nativos de Dataverse

Para escenarios donde solo necesitas notificar a un servicio externo cuando algo pasa, Dataverse tiene webhooks nativos. No necesitas escribir código de plugin.

El proceso es:

  1. En Plugin Registration Tool, registra un "Service Endpoint" de tipo WebHook
  2. Proporciona la URL de tu Azure Function (u otro endpoint HTTP)
  3. Registra un "Step" asociado a mensajes específicos (Create de Account, por ejemplo)

Cuando el evento ocurre, Dataverse hace POST a tu URL con información del contexto. Tu Function recibe los datos y puede procesarlos.


[FunctionName("DataverseWebhook")]
public async Task Run(
    [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req,
    ILogger log)
{
    // Dataverse envía el contexto como RemoteExecutionContext
    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    
    log.LogInformation($"Webhook recibido: {requestBody}");
    
    // Parsear y procesar
    var context = JsonConvert.DeserializeObject(requestBody);
    
    // Hacer lo que necesites...
    
    return new OkResult();
}

Los webhooks son ideales cuando no necesitas modificar datos en Dataverse, solo notificar a sistemas externos.


Eligiendo el patrón correcto

Plugin síncrono directo: Cuando la operación es rápida, necesitas modificar datos de Dataverse, y el usuario debe ver el resultado.

Plugin que llama a Function (síncrono): Cuando necesitas capacidades que el sandbox no permite, pero el usuario aún debe esperar el resultado.

Plugin con Service Bus (fire-and-forget): Cuando la operación puede tomar tiempo, no afecta la experiencia inmediata del usuario, y necesitas garantía de entrega.

Webhook nativo: Cuando solo necesitas notificar a sistemas externos sin lógica adicional en Dataverse.


Puntos clave

  • Azure Functions extienden las capacidades de plugins sin las restricciones del sandbox
  • El patrón de plugin → Function permite operaciones de larga duración
  • Service Bus desacopla completamente y garantiza entrega de mensajes
  • Los webhooks nativos evitan código de plugin para notificaciones simples
  • Elige el patrón según los requisitos de sincronía, durabilidad, y experiencia de usuario

Para profundizar

Inicia sesión e inscríbete para guardar tu progreso.
En este curso
¿Te ha resultado útil?