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:
- En Plugin Registration Tool, registra un "Service Endpoint" de tipo WebHook
- Proporciona la URL de tu Azure Function (u otro endpoint HTTP)
- 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