3.5 Manejo de Excepciones y Validaciones

Aprende a validar y manejar errores correctamente

Un plugin que no maneja errores correctamente es un plugin que causará problemas en producción. El manejo de excepciones no es un tema secundario: es fundamental para crear código robusto que se comporte predeciblemente tanto en casos de éxito como de fallo.

Objetivos de aprendizaje

  • Usar InvalidPluginExecutionException correctamente
  • Implementar validaciones de negocio claras y útiles
  • Manejar errores sin ocultar información valiosa
  • Elegir el stage adecuado para cada tipo de validación

La excepción correcta: InvalidPluginExecutionException

Cuando quieres cancelar una operación y mostrar un mensaje al usuario, usas InvalidPluginExecutionException. Esta es la excepción "oficial" de los plugins de Dataverse, y tiene un tratamiento especial por parte del sistema.


throw new InvalidPluginExecutionException(
    "El descuento no puede ser mayor al 30% sin aprobación gerencial.");

Cuando lanzas esta excepción, el sistema:

  • Cancela la operación actual
  • Revierte la transacción si estás en Pre o Post operation
  • Muestra el mensaje que proporcionaste al usuario en un cuadro de diálogo
  • Incluye todos los traces en el log descargable

El mensaje que proporcionas es lo que ve el usuario final. Por eso es importante que sea claro, orientado al usuario, y que explique qué hacer para resolver el problema.

Mensajes útiles vs mensajes técnicos

Piensa en quién va a leer el mensaje. Un vendedor que está intentando guardar una oportunidad no le sirve saber que hubo un NullReferenceException en la línea 47. Le sirve saber qué campo le falta rellenar o qué condición no se cumple.


// MAL: Mensaje técnico que no ayuda al usuario
throw new InvalidPluginExecutionException(
    "Error: referencia nula en procesamiento de datos");

// BIEN: Mensaje útil que guía al usuario
throw new InvalidPluginExecutionException(
    "Por favor, seleccione un cliente antes de guardar la oportunidad.");

Códigos de error para integraciones

Si tu sistema se integra con otros sistemas o tienes un equipo de soporte que necesita identificar rápidamente tipos de errores, puedes usar códigos de error personalizados:


throw new InvalidPluginExecutionException(
    OperationStatus.Failed,
    1001,  // Código de error personalizado
    "El cliente tiene facturas pendientes y no puede ser eliminado. " +
    "Resuelva las facturas pendientes o contacte a finanzas."
);

El código 1001 aparecerá en los logs, lo que facilita filtrar y categorizar errores cuando estés diagnosticando problemas.


Patrón de validación estructurado

Un plugin de validación bien estructurado tiene varios componentes: verificación del contexto, obtención de datos, validaciones de negocio, y manejo de errores.


public class ValidarDescuentoPlugin : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        var trace = (ITracingService)
            serviceProvider.GetService(typeof(ITracingService));
        var context = (IPluginExecutionContext)
            serviceProvider.GetService(typeof(IPluginExecutionContext));
        
        trace.Trace("ValidarDescuentoPlugin: Inicio");
        
        try
        {
            // Verificar contexto
            if (!context.InputParameters.Contains("Target"))
            {
                trace.Trace("No hay Target, saliendo");
                return;
            }
            
            Entity target = (Entity)context.InputParameters["Target"];
            
            // Verificar si el campo de descuento está siendo modificado
            if (!target.Contains("discountpercentage"))
            {
                trace.Trace("discountpercentage no modificado, saliendo");
                return;
            }
            
            // Obtener el valor
            decimal descuento = target.GetAttributeValue("discountpercentage");
            trace.Trace($"Descuento solicitado: {descuento}%");
            
            // Validar regla de negocio
            if (descuento > 30)
            {
                trace.Trace($"Descuento {descuento}% excede el límite, rechazando");
                throw new InvalidPluginExecutionException(
                    $"El descuento del {descuento}% excede el máximo permitido del 30%. " +
                    "Para descuentos mayores, solicite aprobación a su gerente.");
            }
            
            trace.Trace("Validación pasada");
            trace.Trace("ValidarDescuentoPlugin: Fin");
        }
        catch (InvalidPluginExecutionException)
        {
            throw; // Re-lanzar sin modificar
        }
        catch (Exception ex)
        {
            trace.Trace($"Error inesperado: {ex.Message}");
            trace.Trace(ex.StackTrace);
            throw new InvalidPluginExecutionException(
                "Error interno al validar el descuento. Contacte al administrador.", ex);
        }
    }
}

El catch diferenciado

Observa el patrón de catch al final. Hay dos bloques: uno para InvalidPluginExecutionException y otro para Exception general.

El primero re-lanza sin modificar. Esto es importante porque cuando tú mismo lanzas una InvalidPluginExecutionException, quieres que llegue tal cual al usuario. Si la envuelves en otra excepción, puedes perder el mensaje original.

El segundo es para errores inesperados: excepciones de la base de datos, errores de serialización, cualquier cosa que no hayas anticipado. Aquí logueas el error completo para diagnóstico, pero muestras un mensaje genérico al usuario (no quieres exponerle detalles técnicos o de seguridad).


Agregando múltiples validaciones

En plugins de negocio reales, normalmente tienes varias validaciones. El usuario se frustra si tiene que guardar, ver un error, corregir, guardar, ver otro error, corregir, y así sucesivamente. Es mejor validar todo junto y mostrar todos los problemas de una vez.


public class ValidarOportunidadPlugin : IPlugin
{
    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"];
        
        List errores = new List();
        
        // Validación 1: Cliente asignado
        if (!target.Contains("parentaccountid") && !target.Contains("parentcontactid"))
        {
            errores.Add("Debe seleccionar un cliente (cuenta o contacto).");
        }
        
        // Validación 2: Valor estimado
        Money valor = target.GetAttributeValue("estimatedvalue");
        if (valor == null || valor.Value <= 0)
        {
            errores.Add("El valor estimado debe ser mayor a cero.");
        }
        
        // Validación 3: Fecha de cierre
        DateTime? fechaCierre = target.GetAttributeValue("estimatedclosedate");
        if (fechaCierre.HasValue && fechaCierre.Value < DateTime.Today)
        {
            errores.Add("La fecha de cierre estimada no puede ser en el pasado.");
        }
        
        // Si hay errores, mostrarlos todos juntos
        if (errores.Count > 0)
        {
            string mensajeCompleto = string.Join("\n• ", errores);
            throw new InvalidPluginExecutionException(
                $"Por favor corrija los siguientes problemas:\n• {mensajeCompleto}");
        }
        
        trace.Trace("Todas las validaciones pasadas");
    }
}

Eligiendo el stage correcto para validaciones

No todas las validaciones deben ir en el mismo stage. La elección afecta cuándo se ejecuta tu código y qué recursos consume.

Pre-validation: Ideal para validaciones simples que no necesitan consultar la base de datos. El formato de un email, que un campo obligatorio tenga valor, que un número esté en un rango válido. Al estar fuera de la transacción, rechazar aquí es más eficiente.

Pre-operation: Para validaciones que necesitan leer otros registros. Verificar que el cliente tiene crédito disponible, que no hay otra oportunidad abierta con el mismo producto, que el vendedor tiene asignada esa región.

Post-operation: Raramente se usa para validaciones porque la operación ya ha ocurrido. Pero hay casos: validar que el registro creado cumple con reglas que dependen de su ID (por ejemplo, no puede haber más de 3 registros relacionados).


El anti-patrón de silenciar errores

He visto este patrón en código de producción más veces de las que me gustaría admitir:


// NUNCA hagas esto
try
{
    // código que puede fallar...
}
catch (Exception ex)
{
    // "Manejo" del error
    return; // Silenciar y continuar
}

Esto es peligroso por varias razones. El usuario no sabe que algo falló. Los datos pueden quedar inconsistentes. Cuando eventualmente algo rompa de forma visible, será imposible diagnosticar porque los errores se ocultaron.

Si un error ocurre, el usuario debe saberlo. Puede que el mensaje sea genérico ("Error interno, contacte al administrador"), pero debe existir. Y siempre, siempre, loguea el error real para que puedas diagnosticarlo después.


Puntos clave

  • Usa InvalidPluginExecutionException para cancelar operaciones y mostrar mensajes al usuario
  • Los mensajes deben ser útiles para el usuario final, no técnicos
  • Usa códigos de error personalizados para facilitar diagnósticos
  • Agrupa validaciones para mostrar todos los problemas de una vez
  • Pre-validation para validaciones simples, Pre-operation para las que requieren consultas
  • Nunca silencies excepciones, siempre informa al usuario y loguea el error real

Para profundizar

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