1.4 Contexto de Ejecución y Servicios Disponibles

Aprende a trabajar con el contexto y los servicios disponibles

Cuando tu plugin se ejecuta, no está solo. Dataverse le proporciona acceso a información sobre la operación y a servicios que puede usar para interactuar con el sistema. Saber cómo obtener y usar estos recursos es la base de todo lo que construirás.

Objetivos de aprendizaje

  • Dominar el IPluginExecutionContext y todas sus propiedades útiles
  • Conocer los servicios disponibles a través del IServiceProvider
  • Entender la diferencia entre UserId e InitiatingUserId
  • Saber usar SharedVariables para comunicación entre plugins

El punto de entrada: IServiceProvider

Todo plugin implementa un único método llamado Execute que recibe un parámetro: IServiceProvider. Este objeto es tu puerta de acceso a todo lo que Dataverse te ofrece durante la ejecución.

Piensa en IServiceProvider como un directorio de servicios. No contiene los servicios directamente, pero sabe cómo obtenerlos. Cuando necesitas algo, le pides el servicio por su tipo y él te lo devuelve.


public void Execute(IServiceProvider serviceProvider)
{
    // Obtener el contexto de ejecución
    IPluginExecutionContext context = 
        (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));
    
    // Obtener el servicio de tracing para logging
    ITracingService tracingService = 
        (ITracingService)serviceProvider.GetService(typeof(ITracingService));
    
    // Obtener la fábrica de servicios de organización
    IOrganizationServiceFactory factory = 
        (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
    
    // Ahora puedes trabajar con estos servicios
}

Estos tres servicios son los que usarás en prácticamente todos los plugins: el contexto para saber qué está pasando, el tracing para escribir logs, y la fábrica para obtener un cliente que pueda hablar con Dataverse.


IPluginExecutionContext: todo sobre la operación actual

El contexto de ejecución es probablemente el objeto más importante que tendrás en tus manos. Contiene toda la información sobre la operación que disparó tu plugin: quién la inició, qué tipo de operación es, qué datos están involucrados, y el estado del pipeline.

Identificando la operación

Las primeras propiedades que consultarás son las que te dicen qué está pasando:

MessageName te dice qué operación es: "Create", "Update", "Delete", "Assign", etc. Útil cuando registras un plugin en múltiples mensajes y necesitas comportamiento diferente para cada uno.

PrimaryEntityName es el nombre lógico de la entidad: "account", "contact", "opportunity". También útil para plugins registrados en múltiples entidades.

PrimaryEntityId es el GUID del registro afectado. En Create Pre-operation este valor es Guid.Empty porque el registro todavía no existe.

Stage indica en qué etapa del pipeline estás: 10 para Pre-validation, 20 para Pre-operation, 40 para Post-operation.

Mode te dice si la ejecución es síncrona (0) o asíncrona (1).


// Ejemplo: plugin que maneja Create y Update de forma diferente
if (context.MessageName == "Create")
{
    // Lógica específica para creación
}
else if (context.MessageName == "Update")
{
    // Lógica específica para actualización
}

// Verificar que estamos en el stage correcto
if (context.Stage != 20) // No estamos en Pre-operation
{
    return; // Salir si no es la etapa esperada
}

Los usuarios involucrados: una distinción que importa

El contexto tiene dos propiedades relacionadas con usuarios que confunden a muchos desarrolladores: UserId e InitiatingUserId. Entender la diferencia es fundamental.

InitiatingUserId es el usuario real que inició la operación. Si Laura hace clic en guardar, InitiatingUserId es el ID de Laura. Este valor no cambia a lo largo del pipeline.

UserId es el usuario en cuyo contexto se ejecuta el plugin. Por defecto es el mismo que InitiatingUserId, pero el plugin puede haber sido registrado para ejecutarse en el contexto de otro usuario.

¿Por qué importa? Piensa en seguridad. Cuando tu plugin hace consultas u operaciones a través del IOrganizationService, Dataverse aplica los permisos del usuario. Si usas el servicio creado con UserId, las operaciones respetarán los permisos de ese usuario. Si el usuario no tiene acceso a ciertos registros, tu plugin tampoco los verá.

Esto es generalmente lo que quieres: el plugin opera con los mismos permisos que el usuario que lo disparó. Pero a veces necesitas más privilegios.


IOrganizationService: tu conexión con Dataverse

Para hacer cualquier operación en Dataverse desde tu plugin, necesitas un IOrganizationService. Este servicio te permite crear, leer, actualizar y eliminar registros, ejecutar consultas, y llamar a cualquier API de Dataverse.

Obtenerlo requiere dos pasos: primero obtener la fábrica, luego crear el servicio indicando en contexto de qué usuario quieres operar.


// Obtener la fábrica
IOrganizationServiceFactory factory = 
    (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));

// Crear servicio en contexto del usuario que llama
IOrganizationService serviceAsUser = factory.CreateOrganizationService(context.UserId);

// Crear servicio en contexto del sistema (SYSTEM user)
IOrganizationService serviceAsSystem = factory.CreateOrganizationService(null);

El poder y el peligro de null

Cuando pasas null a CreateOrganizationService, obtienes un servicio que opera como el usuario SYSTEM. Este usuario tiene permisos completos sobre todo el sistema. No hay registro que no pueda leer, no hay operación que no pueda realizar.

Esto es tremendamente útil cuando tu plugin necesita acceder a datos que el usuario actual no puede ver. Por ejemplo, un plugin que verifica configuraciones globales almacenadas en una tabla a la que solo administradores tienen acceso.

Pero es también peligroso. Un plugin que usa el contexto SYSTEM puede saltarse toda la seguridad que has configurado en el sistema. Úsalo solo cuando realmente lo necesites, y sé consciente de las implicaciones.

Consejo de seguridad: Cuando uses CreateOrganizationService(null), documenta claramente por qué es necesario. En auditorías de seguridad, estos son los primeros puntos que se revisan. Tener una justificación clara te ahorrará explicaciones incómodas.

Operaciones básicas con el servicio

Una vez tienes el IOrganizationService, puedes hacer prácticamente cualquier cosa:


// Crear un registro
Entity nuevoContacto = new Entity("contact");
nuevoContacto["firstname"] = "Carlos";
nuevoContacto["lastname"] = "García";
Guid nuevoId = service.Create(nuevoContacto);

// Leer un registro
Entity cuenta = service.Retrieve("account", accountId, new ColumnSet("name", "revenue"));

// Actualizar un registro
Entity actualizacion = new Entity("account", accountId);
actualizacion["revenue"] = new Money(100000);
service.Update(actualizacion);

// Eliminar un registro
service.Delete("contact", contactId);

// Ejecutar consultas
QueryExpression query = new QueryExpression("opportunity")
{
    ColumnSet = new ColumnSet("name", "estimatedvalue"),
    Criteria = new FilterExpression()
};
query.Criteria.AddCondition("statecode", ConditionOperator.Equal, 0);
EntityCollection resultados = service.RetrieveMultiple(query);

ITracingService: dejando rastro para debugging

Los plugins se ejecutan en servidores a los que no tienes acceso directo. No puedes poner breakpoints ni ver la consola. El TracingService es tu forma de dejar mensajes que luego puedes recuperar para entender qué pasó.


ITracingService trace = 
    (ITracingService)serviceProvider.GetService(typeof(ITracingService));

trace.Trace("Plugin iniciado para {0}", context.PrimaryEntityName);
trace.Trace("Operación: {0}, Stage: {1}", context.MessageName, context.Stage);
trace.Trace("Usuario: {0}", context.InitiatingUserId);

// Trazar datos del Target
if (context.InputParameters.Contains("Target"))
{
    Entity target = (Entity)context.InputParameters["Target"];
    trace.Trace("Target contiene {0} atributos", target.Attributes.Count);
}

Los mensajes de trace se acumulan durante la ejecución. Si el plugin completa con éxito, normalmente no los ves. Pero si el plugin lanza una excepción, todos los traces se incluyen en el mensaje de error, lo que hace el debugging mucho más fácil.

También puedes ver los traces habilitando el Plugin Trace Log en la configuración del sistema, o usando el Plugin Profiler para debugging en desarrollo.

Buena práctica: Escribe traces generosamente durante el desarrollo. Son gratuitos en términos de rendimiento (cuando están deshabilitados) y valen oro cuando necesitas diagnosticar un problema en producción.

Depth: evitando bucles infinitos

Hay una propiedad del contexto que puede salvarte de problemas serios: Depth.

Imagina que escribes un plugin que, cuando se crea un contacto, automáticamente crea una actividad de seguimiento. Y tienes otro plugin que, cuando se crea una actividad, actualiza ciertos campos en el contacto relacionado. Y esa actualización dispara un cálculo que a su vez modifica la actividad...

Esto es un bucle infinito. O lo sería, si Dataverse no tuviera protección.

Cada vez que una operación en un plugin dispara otra operación que a su vez dispara otro plugin, el Depth se incrementa. El primer plugin tiene Depth 1, el siguiente Depth 2, y así sucesivamente. Dataverse limita esto a 8 niveles por defecto. Si llegas al límite, la operación falla.


// Protección contra recursión
if (context.Depth > 2)
{
    trace.Trace("Deteniendo ejecución: Depth = {0}", context.Depth);
    return;
}

// Otra opción: solo ejecutar si es la primera llamada
if (context.Depth > 1)
{
    return; // No soy la operación original, alguien me llamó
}

No siempre quieres bloquear por Depth. A veces un plugin necesita disparar operaciones que a su vez disparan otros plugins, y eso es correcto. Pero debes ser consciente de la cadena que estás creando y asegurarte de que no puede entrar en bucle.


SharedVariables: pasando datos entre plugins

¿Qué pasa si tienes dos plugins en el mismo pipeline y necesitas pasar información del primero al segundo? Por ejemplo, un plugin de Pre-validation calcula un valor que el plugin de Pre-operation necesita usar.

Podrías hacer que el segundo plugin repita el cálculo, pero eso es ineficiente. Mejor opción: SharedVariables.

SharedVariables es un diccionario que persiste durante toda la ejecución del pipeline. Lo que pongas ahí estará disponible para todos los plugins posteriores en la misma cadena de ejecución.


// En el primer plugin (por ejemplo Pre-validation)
decimal creditoCalculado = CalcularCredito(cliente);
context.SharedVariables["CreditoCalculado"] = creditoCalculado;
trace.Trace("Crédito calculado y guardado: {0}", creditoCalculado);

// En el segundo plugin (por ejemplo Pre-operation)
if (context.SharedVariables.Contains("CreditoCalculado"))
{
    decimal credito = (decimal)context.SharedVariables["CreditoCalculado"];
    trace.Trace("Usando crédito previamente calculado: {0}", credito);
    // Usar el valor sin recalcular
}
else
{
    // El primer plugin no se ejecutó, calcular aquí
    decimal credito = CalcularCredito(cliente);
}

Un detalle importante: SharedVariables solo funciona dentro del mismo pipeline de ejecución. Si tu plugin dispara una nueva operación (por ejemplo, crea un registro secundario), esa nueva operación tiene su propio contexto con su propio diccionario de SharedVariables.


ParentContext: navegando la jerarquía

Cuando una operación dispara otra, el plugin de la segunda operación puede acceder al contexto de la primera a través de ParentContext.

Esto es útil para entender la cadena de eventos. Si tu plugin de Delete se dispara porque alguien eliminó una cuenta padre (eliminación en cascada), puedes navegar al ParentContext para ver información sobre esa eliminación de la cuenta.


// Navegar hacia arriba en la cadena de operaciones
IPluginExecutionContext current = context;
while (current != null)
{
    trace.Trace("Nivel {0}: {1} en {2}", 
        current.Depth, 
        current.MessageName, 
        current.PrimaryEntityName);
    current = current.ParentContext;
}

// Ejemplo de salida:
// Nivel 3: Delete en contact
// Nivel 2: Delete en account
// Nivel 1: Delete en parentaccount

También puedes acceder a SharedVariables del contexto padre. Esto permite pasar información hacia abajo en la cadena: el plugin del registro padre puede guardar algo en SharedVariables, y los plugins de los registros hijos pueden leerlo navegando al ParentContext.


Pre-Images y Post-Images: referencias rápidas

Aunque los veremos en detalle más adelante, el contexto tiene dos colecciones importantes: PreEntityImages y PostEntityImages. Contienen instantáneas del registro antes y después de la operación.

Si necesitas saber cuál era el valor de un campo antes de que el usuario lo modificara, lo buscas en PreEntityImages. Si necesitas ver el estado final del registro después de la operación, lo buscas en PostEntityImages.

Estas imágenes se configuran al registrar el plugin, indicando qué campos quieres capturar. No son automáticas: debes definir explícitamente qué quieres y con qué nombre.


// Acceder a un Pre-Image configurado con el alias "preImage"
if (context.PreEntityImages.Contains("preImage"))
{
    Entity imagenAnterior = context.PreEntityImages["preImage"];
    string nombreAnterior = imagenAnterior.GetAttributeValue("name");
    trace.Trace("Nombre anterior: {0}", nombreAnterior);
}

Puntos clave

  • IServiceProvider es el punto de entrada para obtener todos los servicios que necesitas
  • IPluginExecutionContext contiene toda la información sobre la operación actual: mensaje, entidad, usuarios, datos
  • InitiatingUserId es quien realmente inició la operación; UserId es el contexto de seguridad del plugin
  • CreateOrganizationService(null) da acceso completo como SYSTEM: poderoso pero peligroso
  • ITracingService es esencial para debugging; escribe traces generosamente
  • Verifica context.Depth para evitar bucles infinitos en cadenas de plugins
  • SharedVariables permite pasar datos entre plugins del mismo pipeline
  • ParentContext te deja navegar hacia operaciones que dispararon la actual

Para profundizar

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