Hemos cubierto teoría, patrones, y prácticas. Ahora veamos plugins completos del mundo real que combinan todo lo aprendido. Estos ejemplos muestran cómo aplicar los conceptos en escenarios que probablemente encuentres en tu trabajo.
Objetivos de aprendizaje
- Ver implementaciones completas de plugins reales
- Entender cómo combinar patrones en escenarios complejos
- Aplicar lo aprendido a tus propios proyectos
Caso 1: Auto-numeración de facturas
Uno de los casos más comunes: generar números secuenciales para facturas, pedidos, o cualquier entidad que requiera identificadores únicos.
Requisitos:
- Formato: FAC-2024-00001
- Secuencial sin saltos
- Cambio de año reinicia el contador
- Concurrencia segura
/// <summary>
/// Genera número de factura secuencial.
/// Registro: Create on invoice, Pre-operation, Synchronous
/// </summary>
public class FacturaAutoNumeracion : PluginBase
{
// Usamos una entidad auxiliar para guardar el contador
private const string COUNTER_ENTITY = "new_autonumber";
protected override void ExecutePlugin(LocalPluginContext ctx)
{
var target = ctx.GetTarget();
ctx.TracingService.Trace("Generando número de factura...");
int year = DateTime.UtcNow.Year;
int nextNumber = GetNextNumber(ctx.OrganizationService, year, ctx.TracingService);
string invoiceNumber = $"FAC-{year}-{nextNumber:D5}";
target["invoicenumber"] = invoiceNumber;
ctx.TracingService.Trace($"Número asignado: {invoiceNumber}");
}
private int GetNextNumber(IOrganizationService service, int year, ITracingService trace)
{
// Buscar o crear contador para este año
var query = new QueryExpression(COUNTER_ENTITY);
query.ColumnSet = new ColumnSet("new_currentvalue");
query.Criteria.AddCondition("new_name", ConditionOperator.Equal, $"INVOICE-{year}");
var result = service.RetrieveMultiple(query);
Entity counter;
int currentValue;
if (result.Entities.Count == 0)
{
// Primer uso del año: crear contador
trace.Trace($"Creando contador para año {year}");
counter = new Entity(COUNTER_ENTITY);
counter["new_name"] = $"INVOICE-{year}";
counter["new_currentvalue"] = 1;
service.Create(counter);
return 1;
}
counter = result.Entities[0];
currentValue = counter.GetAttributeValue<int>("new_currentvalue");
// Incrementar contador
int nextValue = currentValue + 1;
Entity update = new Entity(COUNTER_ENTITY, counter.Id);
update["new_currentvalue"] = nextValue;
service.Update(update);
trace.Trace($"Contador actualizado: {currentValue} -> {nextValue}");
return nextValue;
}
}
Caso 2: Validación de aprobación de oportunidades
Las oportunidades de alto valor requieren aprobación de gerencia antes de cerrarse como ganadas.
Requisitos:
- Oportunidades con valor estimado > $50,000 requieren aprobación
- No se pueden cerrar como ganadas sin aprobación
- Mensaje claro de error
/// <summary>
/// Valida aprobación antes de cerrar oportunidades de alto valor.
/// Registro: Update on opportunity, Pre-validation, Synchronous
/// Filtering Attributes: statecode
/// </summary>
public class OpportunityApprovalValidation : PluginBase
{
private const decimal THRESHOLD = 50000m;
protected override void ExecutePlugin(LocalPluginContext ctx)
{
var target = ctx.GetTarget();
// Solo nos interesa cuando cambia statecode
if (!target.Contains("statecode")) return;
var newState = target.GetAttributeValue<OptionSetValue>("statecode");
// statecode 1 = Won
if (newState?.Value != 1) return;
ctx.TracingService.Trace("Validando aprobación para cierre ganado...");
// Obtener datos completos de la oportunidad
Entity opportunity = ctx.OrganizationService.Retrieve(
"opportunity",
ctx.Context.PrimaryEntityId,
new ColumnSet("estimatedvalue", "new_approved", "name"));
Money estimatedValue = opportunity.GetAttributeValue<Money>("estimatedvalue");
bool isApproved = opportunity.GetAttributeValue<bool>("new_approved");
string name = opportunity.GetAttributeValue<string>("name");
decimal amount = estimatedValue?.Value ?? 0;
ctx.TracingService.Trace($"Oportunidad: {name}");
ctx.TracingService.Trace($"Valor estimado: {amount:C}");
ctx.TracingService.Trace($"Aprobada: {isApproved}");
if (amount > THRESHOLD && !isApproved)
{
throw new InvalidPluginExecutionException(
$"Las oportunidades con valor superior a {THRESHOLD:C} " +
"requieren aprobación de gerencia antes de cerrarse como ganadas. " +
"Solicita aprobación y marca el campo 'Aprobada' antes de continuar.");
}
ctx.TracingService.Trace("Validación exitosa");
}
}
Caso 3: Sincronización con ERP externo
Cuando se crea o actualiza una cuenta, sincronizar los datos con el sistema ERP de la empresa.
Requisitos:
- Sincronización no debe bloquear al usuario
- Si el ERP falla, reintentar automáticamente
- Logging detallado para troubleshooting
/// <summary>
/// Sincroniza cuentas con ERP externo.
/// Registro: Create/Update on account, Post-operation, Asynchronous
/// </summary>
public class AccountSyncToERP : PluginBase
{
private const string ERP_ENDPOINT = "https://erp.miempresa.com/api/customers";
protected override void ExecutePlugin(LocalPluginContext ctx)
{
ctx.TracingService.Trace("Iniciando sincronización con ERP...");
// Obtener datos actuales de la cuenta
Entity account = ctx.OrganizationService.Retrieve(
"account",
ctx.Context.PrimaryEntityId,
new ColumnSet("name", "accountnumber", "telephone1",
"address1_line1", "address1_city", "emailaddress1"));
// Construir payload para el ERP
var payload = new
{
externalId = account.Id.ToString(),
name = account.GetAttributeValue<string>("name"),
accountNumber = account.GetAttributeValue<string>("accountnumber"),
phone = account.GetAttributeValue<string>("telephone1"),
address = account.GetAttributeValue<string>("address1_line1"),
city = account.GetAttributeValue<string>("address1_city"),
email = account.GetAttributeValue<string>("emailaddress1"),
lastModified = DateTime.UtcNow
};
ctx.TracingService.Trace($"Payload: {JsonConvert.SerializeObject(payload)}");
// Enviar al ERP con reintentos
SendToERP(payload, ctx.TracingService);
ctx.TracingService.Trace("Sincronización completada");
}
private void SendToERP(object payload, ITracingService trace)
{
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
Exception lastError = null;
int maxRetries = 3;
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
trace.Trace($"Intento {attempt} de {maxRetries}");
using (var client = new HttpClient())
{
client.Timeout = TimeSpan.FromSeconds(30);
client.DefaultRequestHeaders.Add("Authorization",
$"Bearer {GetApiKey()}");
var json = JsonConvert.SerializeObject(payload);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = client.PostAsync(ERP_ENDPOINT, content).Result;
trace.Trace($"Response: {response.StatusCode}");
if (response.IsSuccessStatusCode)
{
trace.Trace("Sincronización exitosa");
return;
}
string errorBody = response.Content.ReadAsStringAsync().Result;
trace.Trace($"Error body: {errorBody}");
// Solo reintentar en errores de servidor
if ((int)response.StatusCode < 500)
{
throw new InvalidPluginExecutionException(
$"Error del ERP: {response.StatusCode} - {errorBody}");
}
lastError = new Exception($"Server error: {response.StatusCode}");
}
}
catch (TaskCanceledException ex)
{
trace.Trace($"Timeout en intento {attempt}");
lastError = ex;
}
catch (HttpRequestException ex)
{
trace.Trace($"Error de conexión: {ex.Message}");
lastError = ex;
}
if (attempt < maxRetries)
{
int delay = 1000 * attempt;
trace.Trace($"Esperando {delay}ms antes de reintentar...");
System.Threading.Thread.Sleep(delay);
}
}
throw new InvalidPluginExecutionException(
$"No se pudo sincronizar con ERP después de {maxRetries} intentos",
lastError);
}
private string GetApiKey()
{
// En producción, usar Secure Configuration o Key Vault
return "api-key-from-config";
}
}
Recapitulación del curso
Has completado el recorrido desde los fundamentos hasta patrones avanzados de desarrollo de plugins. Ahora tienes las herramientas para:
- Entender cómo funciona el pipeline de ejecución de Dataverse
- Configurar tu entorno de desarrollo correctamente
- Escribir plugins seguros, eficientes, y mantenibles
- Depurar problemas con Plugin Profiler y logs
- Automatizar despliegues con CI/CD
- Testear tu código con frameworks modernos
El siguiente paso es practicar. Toma uno de estos ejemplos, adáptalo a tu contexto, y despliégalo en un entorno de desarrollo. Cada plugin que escribas te dará más experiencia y confianza.