Ha llegado el momento que estabas esperando: escribir código que realmente haga algo. En esta lección crearemos tu primer plugin funcional, uno que puedas registrar y ver ejecutándose en tu entorno de Dataverse.
Objetivos de aprendizaje
- Escribir un plugin funcional desde cero
- Entender cada línea de código y por qué está ahí
- Implementar logging efectivo con ITracingService
- Aprender a validar y rechazar operaciones
El plugin más simple del mundo
Empecemos con algo básico: un plugin que simplemente registra información sobre la operación que lo disparó. No hace nada útil en términos de negocio, pero te permite verificar que todo el flujo funciona.
using System;
using Microsoft.Xrm.Sdk;
namespace Contoso.CRM.Plugins
{
public class HelloWorldPlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
// Paso 1: Obtener el servicio de tracing
ITracingService trace = (ITracingService)
serviceProvider.GetService(typeof(ITracingService));
trace.Trace("HelloWorldPlugin: Iniciando ejecución");
// Paso 2: Obtener el contexto de ejecución
IPluginExecutionContext context = (IPluginExecutionContext)
serviceProvider.GetService(typeof(IPluginExecutionContext));
// Paso 3: Registrar información sobre la operación
trace.Trace($"Mensaje: {context.MessageName}");
trace.Trace($"Entidad: {context.PrimaryEntityName}");
trace.Trace($"Id del registro: {context.PrimaryEntityId}");
trace.Trace($"Stage: {context.Stage}");
trace.Trace($"Depth: {context.Depth}");
trace.Trace($"Usuario que inició: {context.InitiatingUserId}");
trace.Trace("HelloWorldPlugin: Ejecución completada");
}
}
}
Analicemos cada parte de este código.
Los using statements
Solo necesitamos dos namespaces para un plugin básico: System para tipos básicos como Guid y Exception, y Microsoft.Xrm.Sdk que contiene todo lo relacionado con plugins.
La interfaz IPlugin
Toda clase que quiera ser un plugin debe implementar IPlugin. Esta interfaz define un único método: Execute(IServiceProvider serviceProvider). No hay constructores especiales, no hay otros métodos. Todo ocurre en Execute.
El IServiceProvider
El parámetro serviceProvider es tu puerta de acceso a todo lo que Dataverse te proporciona. No contiene los servicios directamente, pero sabe cómo obtenerlos. Usamos GetService(typeof(T)) para pedir cada servicio que necesitamos.
El ITracingService
Lo primero que hago siempre es obtener el servicio de tracing. Si algo falla en mi plugin, quiero tener logs que me digan dónde y por qué. Los traces se acumulan durante la ejecución y se pueden ver en el Plugin Trace Log o en los mensajes de error si el plugin falla.
El IPluginExecutionContext
El contexto contiene toda la información sobre la operación actual. En el ejemplo anterior logueo las propiedades más importantes, pero hay muchas más que veremos a lo largo del curso.
Un plugin que hace algo útil: validación
El ejemplo anterior es bueno para verificar que el flujo funciona, pero no aporta valor de negocio. Vamos a crear algo más práctico: un plugin que valida que las cuentas tengan un número de teléfono antes de poder crearse.
using System;
using Microsoft.Xrm.Sdk;
namespace Contoso.CRM.Plugins.Account
{
/// <summary>
/// Valida que las cuentas tengan número de teléfono.
/// Registro: Create on account, Pre-validation, Synchronous
/// </summary>
public class RequirePhoneOnAccountPlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
// Obtener servicios
ITracingService trace = (ITracingService)
serviceProvider.GetService(typeof(ITracingService));
IPluginExecutionContext context = (IPluginExecutionContext)
serviceProvider.GetService(typeof(IPluginExecutionContext));
trace.Trace("RequirePhoneOnAccountPlugin: Inicio");
// Validar que estamos en el contexto correcto
if (context.MessageName != "Create")
{
trace.Trace("No es Create, saliendo");
return;
}
if (context.PrimaryEntityName != "account")
{
trace.Trace("No es account, saliendo");
return;
}
// Obtener el Target
if (!context.InputParameters.Contains("Target"))
{
trace.Trace("No hay Target, saliendo");
return;
}
Entity target = (Entity)context.InputParameters["Target"];
trace.Trace($"Target obtenido: {target.LogicalName}");
// Obtener el teléfono
string telefono = target.GetAttributeValue<string>("telephone1");
trace.Trace($"Teléfono: '{telefono ?? "(vacío)"}'");
// Validar
if (string.IsNullOrWhiteSpace(telefono))
{
throw new InvalidPluginExecutionException(
"El número de teléfono es obligatorio para crear una cuenta. " +
"Por favor, complete el campo 'Teléfono principal'.");
}
trace.Trace("Validación pasada, teléfono presente");
trace.Trace("RequirePhoneOnAccountPlugin: Fin");
}
}
}
Validaciones de seguridad al principio
Observa las validaciones al inicio: verifico MessageName, PrimaryEntityName, y la existencia del Target. ¿Por qué? Porque aunque registres el plugin solo para Create de account, es buena práctica validar.
En el futuro, alguien podría cambiar el registro por error. O podrías copiar el código a otro plugin y olvidar ajustar algo. Estas validaciones actúan como red de seguridad.
GetAttributeValue: acceso seguro a campos
En lugar de acceder al campo directamente con target["telephone1"], uso GetAttributeValue<string>. Este método devuelve null si el campo no existe, en lugar de lanzar una excepción. Es más seguro para campos opcionales.
InvalidPluginExecutionException: la forma correcta de rechazar
Cuando quieres cancelar una operación y mostrar un mensaje al usuario, lanzas InvalidPluginExecutionException. Esta excepción especial le dice a Dataverse: "rechaza esta operación y muestra este mensaje al usuario".
El mensaje que proporcionas aparece directamente en la interfaz, así que debe ser claro y orientado al usuario, no técnico. "El número de teléfono es obligatorio" es mucho mejor que "NullReferenceException en línea 42".
Un plugin que modifica datos
Las validaciones son útiles, pero a veces quieres que el plugin haga algo con los datos. Aquí tienes un ejemplo que genera un código de cliente automáticamente:
using System;
using Microsoft.Xrm.Sdk;
namespace Contoso.CRM.Plugins.Account
{
/// <summary>
/// Genera un código de cliente automático.
/// Registro: Create on account, Pre-operation, Synchronous
/// </summary>
public class GenerateAccountCodePlugin : IPlugin
{
public void Execute(IServiceProvider serviceProvider)
{
ITracingService trace = (ITracingService)
serviceProvider.GetService(typeof(ITracingService));
IPluginExecutionContext context = (IPluginExecutionContext)
serviceProvider.GetService(typeof(IPluginExecutionContext));
trace.Trace("GenerateAccountCodePlugin: Inicio");
// Validaciones
if (context.MessageName != "Create") return;
if (context.PrimaryEntityName != "account") return;
if (!context.InputParameters.Contains("Target")) return;
Entity target = (Entity)context.InputParameters["Target"];
// Verificar si ya tiene código (por si viene de importación)
string codigoExistente = target.GetAttributeValue<string>("accountnumber");
if (!string.IsNullOrWhiteSpace(codigoExistente))
{
trace.Trace($"Ya tiene código: {codigoExistente}, no generamos");
return;
}
// Generar nuevo código
// En un caso real, consultarías el último número usado
string timestamp = DateTime.UtcNow.ToString("yyyyMMddHHmmss");
string codigo = $"ACC-{timestamp}";
trace.Trace($"Código generado: {codigo}");
// Modificar el Target para incluir el código
target["accountnumber"] = codigo;
trace.Trace("GenerateAccountCodePlugin: Fin");
}
}
}
Modificando el Target
El punto clave de este plugin está en la línea target["accountnumber"] = codigo. En Pre-operation, el objeto target es el que Dataverse va a guardar. Cualquier modificación que hagas aquí se refleja en el registro final.
No necesitas llamar a ningún método especial para guardar. El sistema se encarga automáticamente cuando el pipeline continúa.
Verificando datos existentes
Antes de generar el código, verifico si ya tiene uno. Esto es importante para escenarios de importación de datos: si alguien importa cuentas desde un sistema externo que ya tienen códigos, no quieres sobrescribirlos.
Compilando y verificando
Una vez que tienes tu plugin escrito, compila el proyecto con Ctrl+Shift+B o Build → Build Solution.
Verifica que:
La compilación termina sin errores. Si ves errores, lee los mensajes cuidadosamente. Los más comunes son: falta el paquete NuGet de CoreAssemblies, o usas alguna característica de .NET que no existe en Framework 4.6.2.
El archivo DLL existe en la carpeta bin/Debug (o bin/Release si compilas en Release). Este es el archivo que subirás a Dataverse.
El assembly está firmado. Puedes verificarlo con el comando sn -vf TuProyecto.dll desde Developer Command Prompt.
Próximos pasos
Ahora tienes un plugin compilado y listo. En la siguiente lección veremos cómo usar el Plugin Registration Tool para registrarlo en tu entorno de desarrollo y verlo en acción.
Puntos clave
- Los plugins implementan IPlugin con un único método Execute()
- Siempre obtén ITracingService primero para tener logs disponibles
- Valida MessageName, entidad y existencia del Target al inicio
- Usa GetAttributeValue<T> para acceso seguro a campos
- InvalidPluginExecutionException cancela la operación y muestra mensaje al usuario
- En Pre-operation, modificar el Target afecta el registro que se guarda
- Compila y verifica que el DLL existe y está firmado antes de intentar registrar