En un mundo conectado, los plugins rara vez operan aislados. Muchas veces necesitas comunicarte con sistemas externos: ERPs, servicios de email, APIs de terceros, sistemas de pago. Esta lección cubre cómo hacer llamadas HTTP desde plugins, las restricciones del sandbox, y patrones para integraciones robustas.
Objetivos de aprendizaje
- Realizar llamadas HTTP a servicios externos desde plugins sandbox
- Conocer y trabajar dentro de las restricciones del sandbox
- Implementar reintentos y manejo de errores para llamadas externas
- Gestionar credenciales de forma segura
El sandbox y sus restricciones
Los plugins de Dataverse se ejecutan en un entorno sandbox que limita lo que pueden hacer. Esto es por seguridad: imagina un plugin malicioso con acceso completo al sistema de archivos de Microsoft.
Para llamadas HTTP, las restricciones principales son:
Solo HTTPS: Las URLs deben usar https://. Las llamadas HTTP sin encriptar están bloqueadas.
Timeout general: El timeout total del plugin sigue aplicando. En plugins síncronos tienes 2 minutos para todo, incluyendo las llamadas HTTP.
Sin certificados locales: No puedes cargar certificados de cliente desde disco. Si el servicio externo requiere autenticación por certificado, debes usar otros mecanismos.
Puertos limitados: Solo puertos estándar web (443 para HTTPS). Puertos custom no funcionarán.
Realizando llamadas HTTP básicas
Con las restricciones en mente, veamos cómo hacer llamadas HTTP:
using System.Net;
using System.Net.Http;
public void LlamarServicioExterno(string url, string datos, ITracingService trace)
{
trace.Trace($"Llamando a: {url}");
// Forzar TLS 1.2 - muchos servicios modernos lo requieren
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
using (HttpClient client = new HttpClient())
{
// Timeout razonable - dejar margen para el resto del plugin
client.Timeout = TimeSpan.FromSeconds(30);
// Configurar headers si es necesario
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);
client.DefaultRequestHeaders.Add("Accept", "application/json");
// Contenido de la petición
var content = new StringContent(datos, Encoding.UTF8, "application/json");
// Ejecutar la petición
// Nota: usamos .Result porque no hay soporte para async/await en plugins
HttpResponseMessage response = client.PostAsync(url, content).Result;
trace.Trace($"Respuesta: {response.StatusCode}");
if (!response.IsSuccessStatusCode)
{
string errorBody = response.Content.ReadAsStringAsync().Result;
trace.Trace($"Error body: {errorBody}");
throw new InvalidPluginExecutionException(
$"Error llamando al servicio externo: {response.StatusCode}");
}
string responseBody = response.Content.ReadAsStringAsync().Result;
trace.Trace($"Respuesta recibida: {responseBody.Substring(0, Math.Min(200, responseBody.Length))}...");
}
}
Sobre async/await
El código usa `.Result` en lugar de `await`. Esto es porque los plugins de Dataverse no soportan métodos async. El método Execute debe ser síncrono. Aunque esto bloquea el hilo, es la única opción disponible.
Implementando reintentos
Los servicios externos fallan. Redes inestables, servidores sobrecargados, mantenimientos temporales. Un plugin robusto implementa reintentos para errores transitorios:
public HttpResponseMessage LlamarConReintentos(
HttpClient client,
string url,
HttpContent content,
ITracingService trace,
int maxReintentos = 3)
{
Exception ultimoError = null;
for (int intento = 1; intento <= maxReintentos; intento++)
{
try
{
trace.Trace($"Intento {intento} de {maxReintentos}");
HttpResponseMessage response = client.PostAsync(url, content).Result;
// Reintentar solo para errores de servidor (5xx) o timeout
if (response.StatusCode >= HttpStatusCode.InternalServerError ||
response.StatusCode == HttpStatusCode.RequestTimeout)
{
trace.Trace($"Error retriable: {response.StatusCode}");
ultimoError = new HttpRequestException($"Server error: {response.StatusCode}");
}
else
{
return response; // Éxito o error de cliente (4xx) - no reintentar
}
}
catch (TaskCanceledException ex)
{
// Timeout del HttpClient
trace.Trace($"Timeout en intento {intento}");
ultimoError = ex;
}
catch (HttpRequestException ex)
{
trace.Trace($"Error de red en intento {intento}: {ex.Message}");
ultimoError = ex;
}
if (intento < maxReintentos)
{
// Esperar antes de reintentar (exponential backoff simple)
int waitMs = 1000 * intento;
trace.Trace($"Esperando {waitMs}ms antes de reintentar");
System.Threading.Thread.Sleep(waitMs);
}
}
throw new InvalidPluginExecutionException(
$"Servicio no disponible después de {maxReintentos} intentos", ultimoError);
}
El patrón de "exponential backoff" espera más tiempo entre cada reintento. Esto da tiempo al servicio para recuperarse y evita sobrecargarlo con peticiones repetidas.
Gestión segura de credenciales
Nunca pongas credenciales directamente en el código. Aunque el assembly está en Dataverse, las credenciales hardcodeadas son un riesgo de seguridad y dificultan cambios.
Tienes varias opciones:
Secure Configuration: El Plugin Registration Tool permite configurar "Secure Config" y "Unsecure Config" para cada step. El Secure Config está encriptado y solo accesible en runtime.
public class MiPlugin : IPlugin
{
private readonly string _secureConfig;
// El constructor recibe la configuración
public MiPlugin(string unsecure, string secure)
{
_secureConfig = secure;
}
public void Execute(IServiceProvider serviceProvider)
{
// Parsear la configuración (puede ser JSON, key=value, etc.)
var config = JsonConvert.DeserializeObject(_secureConfig);
string apiKey = config.ApiKey;
// Usar apiKey para llamadas HTTP
}
}
Environment Variables: Dataverse soporta variables de entorno que pueden almacenar secretos. Las obtienes como registros de la entidad environmentvariablevalue.
Azure Key Vault: Para escenarios enterprise, puedes llamar a Azure Key Vault para obtener secretos. Esto agrega latencia pero ofrece mejor gestión de secretos.
Consideraciones de rendimiento
Las llamadas HTTP son lentas comparadas con operaciones locales. Considera estas recomendaciones:
En plugins síncronos: Minimiza las llamadas externas. El usuario está esperando. Si la llamada puede fallar o tardar, considera si realmente debe ser síncrona.
Usa plugins asíncronos: Para sincronizaciones con sistemas externos, un plugin asíncrono es casi siempre mejor. El usuario continúa trabajando y la sincronización ocurre en background.
Timeouts cortos: No uses el timeout por defecto de HttpClient. Configura timeouts que tengan sentido para tu servicio, dejando margen para el resto del plugin.
Considera Azure Functions: Para integraciones complejas, un plugin puede simplemente llamar a una Azure Function que maneja toda la lógica de integración. Esto desacopla la lógica y permite mejor logging y debugging.
Puntos clave
- Los plugins sandbox pueden hacer llamadas HTTPS a servicios externos
- Usa TLS 1.2 explícitamente y configura timeouts razonables
- Implementa reintentos con exponential backoff para mayor robustez
- Nunca hardcodees credenciales, usa Secure Config o Environment Variables
- Considera que las llamadas HTTP son lentas, usa async cuando sea posible
- Para integraciones complejas, Azure Functions puede ser mejor opción