Un plugin pequeño que valida un campo es fácil de mantener. Pero cuando tienes docenas de plugins, cada uno con su lógica específica, el código puede volverse un caos. Esta lección presenta patrones arquitectónicos que te ayudarán a crear plugins escalables, mantenibles, y testeables.
Objetivos de aprendizaje
- Diseñar una arquitectura de plugins escalable
- Implementar una clase base reutilizable
- Organizar código para proyectos de gran escala
- Separar responsabilidades entre capas
El problema de los plugins monolíticos
El patrón que hemos usado hasta ahora funciona bien para plugins individuales: implementas IPlugin, obtienes los servicios del IServiceProvider, ejecutas tu lógica. Pero cuando el proyecto crece, empiezas a ver problemas:
Código repetido: Cada plugin tiene las mismas 10-15 líneas para obtener servicios. Si quieres cambiar cómo manejas errores, tienes que modificar 30 archivos.
Lógica mezclada: La validación, el cálculo de negocio, y la persistencia están todas en el mismo método Execute. Difícil de testear, difícil de entender.
Sin estructura: Los plugins están todos en la raíz del proyecto. ¿Cuáles son de Account? ¿Cuáles de Opportunity? Hay que leer el código para saberlo.
La solución: una clase base bien diseñada
El primer paso es crear una clase base que encapsule todo el código común:
public abstract class PluginBase : IPlugin
{
protected string PluginClassName { get; }
protected PluginBase()
{
PluginClassName = GetType().Name;
}
public void Execute(IServiceProvider serviceProvider)
{
// Extraer servicios una vez
var context = (IPluginExecutionContext)
serviceProvider.GetService(typeof(IPluginExecutionContext));
var trace = (ITracingService)
serviceProvider.GetService(typeof(ITracingService));
var factory = (IOrganizationServiceFactory)
serviceProvider.GetService(typeof(IOrganizationServiceFactory));
// Crear servicio con permisos del usuario actual
var service = factory.CreateOrganizationService(context.UserId);
// Logging estructurado al inicio
trace.Trace($"=== {PluginClassName} ===");
trace.Trace($"Mensaje: {context.MessageName}");
trace.Trace($"Entidad: {context.PrimaryEntityName}");
trace.Trace($"Stage: {context.Stage}");
trace.Trace($"Depth: {context.Depth}");
trace.Trace($"Usuario: {context.InitiatingUserId}");
// Protección contra loops infinitos
if (context.Depth > 2)
{
trace.Trace("Saliendo: Depth > 2");
return;
}
// Empaquetar todo en un objeto de contexto limpio
var localContext = new LocalPluginContext
{
Context = context,
TracingService = trace,
OrganizationService = service,
ServiceFactory = factory
};
try
{
// Delegar a la implementación concreta
ExecutePlugin(localContext);
trace.Trace($"=== {PluginClassName} completado ===");
}
catch (InvalidPluginExecutionException)
{
// Dejar que las excepciones controladas suban tal cual
throw;
}
catch (Exception ex)
{
// Convertir excepciones inesperadas en InvalidPluginExecutionException
trace.Trace($"ERROR: {ex.Message}");
trace.Trace(ex.StackTrace);
throw new InvalidPluginExecutionException(
$"Error en {PluginClassName}: {ex.Message}", ex);
}
}
// Cada plugin concreto implementa esta función
protected abstract void ExecutePlugin(LocalPluginContext context);
}
// Clase auxiliar para pasar el contexto limpio
public class LocalPluginContext
{
public IPluginExecutionContext Context { get; set; }
public ITracingService TracingService { get; set; }
public IOrganizationService OrganizationService { get; set; }
public IOrganizationServiceFactory ServiceFactory { get; set; }
// Métodos helper comunes
public Entity GetTarget()
{
return (Entity)Context.InputParameters["Target"];
}
public Entity GetPreImage(string alias = "PreImage")
{
return Context.PreEntityImages.Contains(alias)
? Context.PreEntityImages[alias]
: null;
}
public Entity GetPostImage(string alias = "PostImage")
{
return Context.PostEntityImages.Contains(alias)
? Context.PostEntityImages[alias]
: null;
}
}
Con esta estructura, tus plugins concretos se simplifican dramáticamente:
public class AccountPreCreate : PluginBase
{
protected override void ExecutePlugin(LocalPluginContext ctx)
{
ctx.TracingService.Trace("Procesando creación de cuenta...");
var target = ctx.GetTarget();
// Tu lógica aquí, sin preocuparte por obtener servicios
// ni manejar excepciones generales
if (string.IsNullOrEmpty(target.GetAttributeValue("name")))
{
throw new InvalidPluginExecutionException(
"El nombre de la cuenta es obligatorio.");
}
// Asignar número de cuenta
target["accountnumber"] = GenerarNumeroCuenta(ctx.OrganizationService);
}
private string GenerarNumeroCuenta(IOrganizationService service)
{
// Lógica de generación...
return "ACC-" + DateTime.Now.Ticks;
}
}
Organización de carpetas
Para proyectos con muchos plugins, organiza por entidad y luego por funcionalidad:
MiEmpresa.Plugins/
├── Base/
│ ├── PluginBase.cs
│ └── LocalPluginContext.cs
├── Account/
│ ├── AccountPreCreate.cs
│ ├── AccountPostUpdate.cs
│ └── AccountValidation.cs
├── Contact/
│ ├── ContactPreCreate.cs
│ └── ContactDeduplication.cs
├── Opportunity/
│ ├── OpportunityCalculations.cs
│ └── OpportunityWorkflow.cs
├── Services/
│ ├── INumberingService.cs
│ ├── NumberingService.cs
│ ├── INotificationService.cs
│ └── NotificationService.cs
├── Helpers/
│ ├── EntityExtensions.cs
│ └── ValidationHelper.cs
└── Constants/
├── EntityNames.cs
└── FieldNames.cs
Esta estructura hace que sea inmediatamente obvio dónde encontrar cada plugin y dónde agregar nuevos.
Separación de responsabilidades
El plugin no debería contener lógica de negocio compleja directamente. Debería coordinar servicios que sí la contienen:
// Servicio con lógica de negocio
public interface IDiscountCalculator
{
decimal Calculate(Entity customer, decimal orderAmount);
}
public class DiscountCalculator : IDiscountCalculator
{
private readonly IOrganizationService _service;
public DiscountCalculator(IOrganizationService service)
{
_service = service;
}
public decimal Calculate(Entity customer, decimal orderAmount)
{
int level = customer.GetAttributeValue("customerlevel")?.Value ?? 1;
decimal baseDiscount = level switch
{
1 => 0m,
2 => 5m,
3 => 10m,
4 => 15m,
_ => 0m
};
// Descuento adicional por volumen
if (orderAmount > 10000) baseDiscount += 5m;
if (orderAmount > 50000) baseDiscount += 5m;
return Math.Min(baseDiscount, 25m); // Máximo 25%
}
}
// Plugin delgado que orquesta
public class OrderPostCreate : PluginBase
{
protected override void ExecutePlugin(LocalPluginContext ctx)
{
var target = ctx.GetTarget();
var customerRef = target.GetAttributeValue("customerid");
if (customerRef == null) return;
// Obtener cliente
var customer = ctx.OrganizationService.Retrieve(
customerRef.LogicalName, customerRef.Id,
new ColumnSet("customerlevel"));
// Usar servicio para calcular
var calculator = new DiscountCalculator(ctx.OrganizationService);
decimal orderAmount = target.GetAttributeValue("totalamount")?.Value ?? 0;
decimal discount = calculator.Calculate(customer, orderAmount);
// Aplicar descuento
if (discount > 0)
{
target["discountpercentage"] = discount;
ctx.TracingService.Trace($"Descuento aplicado: {discount}%");
}
}
}
Esta separación tiene beneficios claros:
- La lógica del descuento se puede probar sin Dataverse
- Puedes reutilizar DiscountCalculator en otros plugins
- Cambios en la lógica de descuento están centralizados
- El plugin es fácil de entender: solo coordina
Puntos clave
- Crea una PluginBase que encapsule código común: obtención de servicios, logging, manejo de excepciones
- Organiza plugins por entidad en carpetas separadas
- Separa lógica de negocio en servicios independientes
- Los plugins deben ser delgados: coordinar, no implementar
- Usa constantes para nombres de entidades y campos