Las transacciones son uno de los conceptos más importantes y menos comprendidos del desarrollo de plugins. Saber cómo funciona el rollback automático te permite diseñar plugins que se comportan correctamente tanto cuando todo va bien como cuando algo falla.
Objetivos de aprendizaje
- Entender qué stages participan en la transacción
- Conocer el comportamiento del rollback automático
- Identificar operaciones que no se pueden revertir
- Diseñar plugins que manejan correctamente escenarios de fallo
Entendiendo las transacciones en Dataverse
Cuando un usuario guarda un registro en Dataverse, se inicia una transacción de base de datos. Esta transacción agrupa todas las operaciones relacionadas: la escritura del registro principal, los plugins que se ejecutan, las reglas de negocio que aplican.
Si cualquier parte de esta cadena falla, la transacción completa se revierte. Es como si nada hubiera pasado. El registro no se guarda, cualquier cosa que los plugins hayan modificado se deshace.
Pero no todos los plugins participan en esta transacción. El comportamiento varía según el stage y el modo de ejecución:
Pre-validation (Stage 10)
Pre-validation se ejecuta ANTES de que la transacción comience. Si lanzas una excepción aquí, no hay nada que revertir porque todavía no se ha escrito nada en la base de datos.
Esto lo hace ideal para validaciones rápidas: verificar formatos, rechazar valores inválidos y demás comprobaciones que no requieren consultar otros datos. Al estar fuera de la transacción, rechazar aquí es más eficiente.
Pre-operation (Stage 20)
Pre-operation está DENTRO de la transacción. Cuando tu código se ejecuta aquí, la transacción ya comenzó pero la operación principal aún no se ha completado.
Si lanzas una excepción en Pre-operation, se revierte todo lo que haya pasado hasta ese punto, incluyendo cambios hechos por otros plugins que se ejecutaron antes del tuyo.
Post-operation síncrono (Stage 40)
Post-operation síncrono también está dentro de la transacción. En este punto la operación principal ya se completó, pero la transacción sigue abierta.
Si tu plugin Post-operation falla, se revierte todo: la operación principal Y cualquier cosa que hayas hecho tú u otros plugins.
Post-operation asíncrono
Los plugins asíncronos se ejecutan FUERA de la transacción. Cuando tu código asíncrono se ejecuta, la transacción original ya se cerró, el registro ya está guardado en la base de datos.
Si tu plugin asíncrono falla, la operación original no se afecta. El registro sigue existiendo. Debes manejar la inconsistencia de otra forma.
El rollback en acción
Veamos un ejemplo concreto para entender el rollback:
// Plugin en Post-operation síncrono de Create de Account
public void Execute(IServiceProvider serviceProvider)
{
var factory = (IOrganizationServiceFactory)
serviceProvider.GetService(typeof(IOrganizationServiceFactory));
var service = factory.CreateOrganizationService(null);
var trace = (ITracingService)
serviceProvider.GetService(typeof(ITracingService));
// Crear una tarea asociada a la cuenta
Entity tarea = new Entity("task");
tarea["subject"] = "Dar seguimiento a nueva cuenta";
tarea["regardingobjectid"] = new EntityReference("account",
((Entity)((IPluginExecutionContext)serviceProvider
.GetService(typeof(IPluginExecutionContext)))
.InputParameters["Target"]).Id);
Guid tareaId = service.Create(tarea);
trace.Trace($"Tarea creada: {tareaId}");
// Ahora algo falla...
throw new InvalidPluginExecutionException(
"Error intencional para demostrar rollback");
}
Cuando este plugin se ejecuta:
- El usuario intenta crear una cuenta
- La cuenta se inserta en la base de datos (dentro de la transacción)
- Nuestro plugin crea una tarea (también dentro de la transacción)
- Nuestro plugin lanza una excepción
- La transacción se revierte: la cuenta Y la tarea desaparecen
- El usuario ve el mensaje de error
Desde la perspectiva del usuario, es como si nada hubiera pasado. No hay cuenta huérfana ni tarea sin propósito.
Operaciones que no se pueden revertir
El rollback funciona para operaciones dentro de Dataverse. Pero hay cosas que no se pueden deshacer:
Llamadas HTTP a servicios externos: Si tu plugin llama a una API externa y luego falla, la API ya recibió la llamada. No hay forma de "deshacer" una petición HTTP.
Emails enviados: Si usas el servicio de correo de Dataverse para enviar un email, y luego el plugin falla, el email ya se envió.
Archivos escritos: Cualquier operación en sistemas de archivos externos o servicios de almacenamiento no se puede revertir.
Esto tiene implicaciones importantes para el diseño de tus plugins:
// PATRÓN PROBLEMÁTICO
public void Execute(IServiceProvider serviceProvider)
{
// ¡PRIMERO llamamos a API externa!
EnviarDatosASistemaExterno(datos);
// Luego hacemos validaciones
if (!EsValido(datos))
{
// Si esto falla, los datos YA están en el sistema externo
throw new InvalidPluginExecutionException("Datos inválidos");
}
}
// PATRÓN CORRECTO
public void Execute(IServiceProvider serviceProvider)
{
// PRIMERO validamos todo
if (!EsValido(datos))
{
throw new InvalidPluginExecutionException("Datos inválidos");
}
// Operaciones internas de Dataverse
service.Create(registroRelacionado);
// ÚLTIMO: operaciones irrecuperables
try
{
EnviarDatosASistemaExterno(datos);
}
catch (Exception ex)
{
throw new InvalidPluginExecutionException(
"Error sincronizando con sistema externo. Los cambios se han revertido.", ex);
}
}
Diseñando para el fallo
Un buen diseño de plugins considera qué pasa cuando las cosas van mal:
Valida primero, actúa después. Todas las validaciones que pueden fallar deberían ejecutarse antes de hacer cualquier operación que no se pueda deshacer.
Las llamadas externas van al final. Si necesitas sincronizar con un sistema externo, hazlo como último paso. Así, si la sincronización falla, puedes revertir todo lo interno de Dataverse.
Considera el patrón de compensación. Para operaciones externas críticas, puedes implementar lógica de compensación: si la operación B falla después de que A tuvo éxito, llamas a un método para deshacer A.
// Patrón de compensación simple
string externalRecordId = null;
try
{
// Paso 1: Crear en sistema externo
externalRecordId = CrearEnSistemaExterno(datos);
// Paso 2: Actualizar en Dataverse
service.Update(registroLocal);
// Paso 3: Algo más que puede fallar
OperacionQUEPuedeFallar();
}
catch (Exception ex)
{
// Si algo falló después de crear en el sistema externo, compensar
if (externalRecordId != null)
{
try
{
EliminarEnSistemaExterno(externalRecordId);
}
catch
{
// Loguear pero no ocultar el error original
}
}
throw new InvalidPluginExecutionException("Operación fallida", ex);
}
Plugins asíncronos y la ausencia de transacción
Los plugins asíncronos tienen un modelo completamente diferente. Cuando se ejecutan, la transacción original ya terminó. El registro que disparó el plugin ya existe (o ya fue eliminado, si era un Delete).
Si un plugin asíncrono falla:
- La operación original no se afecta
- El plugin se reintenta automáticamente hasta 3 veces
- Si sigue fallando, queda marcado como "Failed" en System Jobs
- Debes manejar la inconsistencia manualmente
Esto hace que los plugins asíncronos sean inadecuados para operaciones que DEBEN completarse junto con la operación principal. Son ideales para tareas secundarias que pueden fallar sin romper el flujo principal: notificaciones, sincronizaciones no críticas, actualización de cachés.
Puntos clave
- Pre-operation y Post-operation síncrono participan en la transacción
- Si un plugin síncrono falla, toda la transacción se revierte automáticamente
- Pre-validation está fuera de la transacción, es eficiente para validaciones rápidas
- Llamadas HTTP, emails, y acciones externas NO se pueden revertir
- Valida primero, haz operaciones irreversibles al final
- Plugins asíncronos no participan en la transacción original