7.5 Casos de Uso Reales y Ejemplos Completos

Ejemplos prácticos de plugins completos del mundo real

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;
    }
}
Nota sobre concurrencia: Este enfoque funciona en la mayoría de casos. Para sistemas de muy alto volumen con creación simultánea masiva, considera usar un servicio externo con locking de base de datos más robusto.

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.


Para profundizar

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