Un plugin que funciona correctamente pero tarda 30 segundos es tan problemático como uno que falla. Los usuarios se frustran, las operaciones agotan tiempo, y el sistema se vuelve inutilizable. Esta lección cubre los anti-patrones más comunes y cómo evitarlos.
Objetivos de aprendizaje
- Identificar anti-patrones de rendimiento
- Optimizar consultas a la base de datos
- Usar operaciones batch eficientemente
- Conocer los límites de la plataforma
Reglas fundamentales
Antes de ver anti-patrones específicos, estas son las reglas que debes tener siempre en mente:
- Minimiza llamadas a la base de datos. Cada Retrieve o RetrieveMultiple es un round-trip al servidor. Son lentos.
- Recupera solo lo que necesitas. ColumnSet(true) es casi siempre incorrecto.
- Evita loops con llamadas. Un loop con Retrieve dentro es N llamadas. Una consulta con IN es 1 llamada.
- Usa batch cuando sea posible. ExecuteMultiple agrupa múltiples operaciones en una sola llamada.
Anti-patrón: ColumnSet(true)
// MAL: Recupera TODOS los campos de la cuenta
// Incluye blobs, campos calculados, autonumber, todo
Entity cuenta = service.Retrieve("account", id, new ColumnSet(true));
// Efecto: consulta muy pesada, mucho más datos de los necesarios
// transferidos por red, más memoria usada
La solución es siempre especificar exactamente qué campos necesitas:
// BIEN: Solo los campos que vas a usar
Entity cuenta = service.Retrieve("account", id,
new ColumnSet("name", "telephone1", "primarycontactid"));
Anti-patrón: Retrieve en loop
// MAL: N llamadas a la base de datos
List<Guid> contactIds = ObtenerContactosRelacionados();
foreach (var id in contactIds)
{
Entity contacto = service.Retrieve("contact", id,
new ColumnSet("fullname", "emailaddress1"));
ProcesarContacto(contacto);
}
Si tienes 100 contactos, son 100 llamadas. Esto es extremadamente ineficiente.
// BIEN: Una sola llamada con IN
List<Guid> contactIds = ObtenerContactosRelacionados();
var query = new QueryExpression("contact");
query.ColumnSet = new ColumnSet("fullname", "emailaddress1");
query.Criteria.AddCondition("contactid", ConditionOperator.In,
contactIds.Cast<object>().ToArray());
EntityCollection contactos = service.RetrieveMultiple(query);
foreach (Entity contacto in contactos.Entities)
{
ProcesarContacto(contacto);
}
Una sola consulta trae todos los contactos de una vez.
Anti-patrón: Create/Update en loop
// MAL: N operaciones separadas
foreach (var item in itemsToCreate)
{
service.Create(item); // Una llamada por cada item
}
Usa ExecuteMultiple para agrupar operaciones:
// BIEN: Una sola llamada con múltiples operaciones
var multiRequest = new ExecuteMultipleRequest
{
Settings = new ExecuteMultipleSettings
{
ContinueOnError = false,
ReturnResponses = false
},
Requests = new OrganizationRequestCollection()
};
foreach (var item in itemsToCreate)
{
multiRequest.Requests.Add(new CreateRequest { Target = item });
}
// Una sola llamada que ejecuta todo
service.Execute(multiRequest);
ExecuteMultiple soporta hasta 1000 operaciones por batch. Para más, divide en múltiples llamadas.
Límites de la plataforma
Conocer estos límites te ayuda a diseñar plugins que no los violen:
Timeout de plugin síncrono: 2 minutos. Si tu plugin tarda más, falla.
Timeout de plugin asíncrono: Aproximadamente 1 hora.
ExecuteMultiple: Máximo 1000 requests por batch.
Tamaño del assembly: Máximo 16 MB.
Depth máximo: 8 niveles de llamadas anidadas.
Llamadas HTTP concurrentes: 2 por plugin (sandbox limitation).
Filtering Attributes: prevención desde el registro
En plugins de Update, cada actualización de la entidad dispara el plugin, incluso si los campos que te importan no cambiaron.
En Plugin Registration Tool, al registrar el step de Update, configura Filtering Attributes:
Filtering Attributes: creditlimit,customerlevel,discountpercentage
Ahora el plugin solo se ejecuta si al menos uno de esos campos cambió. Actualizaciones al teléfono o dirección no disparan ejecución innecesaria.
Pre-validation vs Pre-operation
Pre-validation se ejecuta ANTES de que comience la transacción de base de datos. Si rechazas aquí, es más eficiente porque no hay transacción que revertir.
Usa Pre-validation para:
- Validaciones que no requieren consultar la base de datos
- Verificación de formato de campos
- Rechazar operaciones basándose solo en los datos del Target
Usa Pre-operation para:
- Validaciones que necesitan datos de otros registros
- Modificación del Target antes de guardar
- Cualquier lógica que requiera IOrganizationService
Medir rendimiento
No optimices a ciegas. Mide primero:
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
// Operación que quieres medir
var resultado = service.RetrieveMultiple(query);
stopwatch.Stop();
trace.Trace($"Consulta completada en {stopwatch.ElapsedMilliseconds}ms");
Compara tiempos antes y después de optimizar para asegurar que el cambio realmente mejora las cosas.
Puntos clave
- Nunca uses ColumnSet(true) en producción
- Reemplaza Retrieve en loop por una consulta con IN
- Usa ExecuteMultiple para operaciones batch
- Configura Filtering Attributes para evitar ejecuciones innecesarias
- Pre-validation es más eficiente que Pre-operation para rechazos rápidos
- Mide antes y después de optimizar