5.2 Patrón de Inyección de Dependencias

Crea código modular y testeable con patrones de DI

A medida que tus plugins crecen en complejidad, el código tiende a convertirse en un monolito difícil de mantener y de probar. La inyección de dependencias es un patrón que te ayuda a estructurar el código de forma modular, haciendo más fácil el testing y el mantenimiento a largo plazo.

Objetivos de aprendizaje

  • Entender por qué el código acoplado es problemático
  • Aplicar principios de inyección de dependencias en plugins
  • Crear código testeable y modular
  • Implementar una clase base para plugins reutilizable

El problema del código acoplado

El patrón típico de plugins que hemos visto hasta ahora tiene todo el código en el método Execute:


public class MiPlugin : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)
            serviceProvider.GetService(typeof(IPluginExecutionContext));
        var service = GetOrganizationService(serviceProvider, context);
        
        Entity target = (Entity)context.InputParameters["Target"];
        
        // 100 líneas de lógica de negocio aquí...
        // Cálculos, validaciones, consultas, actualizaciones...
        // Todo mezclado en un solo método gigante
    }
}

Este código tiene varios problemas:

Difícil de testear: Para probar la lógica, necesitas simular todo el IServiceProvider, el contexto, el servicio. Es complicado.

Difícil de reutilizar: Si otro plugin necesita la misma lógica de cálculo, tienes que copiar y pegar.

Difícil de mantener: Cuando cambian los requisitos de negocio, modificar un método de 200 líneas es arriesgado.


Separando la lógica de negocio

La primera mejora es extraer la lógica de negocio a clases separadas:


// Interface que define el contrato
public interface IDescuentoCalculator
{
    decimal Calcular(Entity cliente, decimal montoCompra);
}

// Implementación concreta
public class DescuentoCalculator : IDescuentoCalculator
{
    private readonly IOrganizationService _service;
    
    public DescuentoCalculator(IOrganizationService service)
    {
        _service = service;
    }
    
    public decimal Calcular(Entity cliente, decimal montoCompra)
    {
        int nivel = cliente.GetAttributeValue("customerlevel")?.Value ?? 1;
        
        decimal porcentaje = nivel switch
        {
            1 => 0m,
            2 => 5m,
            3 => 10m,
            4 => 15m,
            _ => 0m
        };
        
        if (montoCompra > 10000) 
            porcentaje += 5m;
        
        return porcentaje;
    }
}

Ahora la lógica de cálculo está aislada. Puedes probarla pasando un Entity de prueba sin necesidad de conectar a Dataverse.


Inyectando dependencias en el plugin

El siguiente paso es que el plugin use la clase de negocio a través de la interface:


public class CalcularDescuentoPlugin : IPlugin
{
    private readonly Func _calculatorFactory;
    
    // Constructor por defecto: usado en producción
    public CalcularDescuentoPlugin() 
        : this(service => new DescuentoCalculator(service))
    {
    }
    
    // Constructor para testing: permite inyectar mock
    public CalcularDescuentoPlugin(
        Func calculatorFactory)
    {
        _calculatorFactory = calculatorFactory;
    }
    
    public void Execute(IServiceProvider serviceProvider)
    {
        var context = (IPluginExecutionContext)
            serviceProvider.GetService(typeof(IPluginExecutionContext));
        var factory = (IOrganizationServiceFactory)
            serviceProvider.GetService(typeof(IOrganizationServiceFactory));
        
        IOrganizationService service = factory.CreateOrganizationService(context.UserId);
        
        Entity target = (Entity)context.InputParameters["Target"];
        decimal monto = target.GetAttributeValue("amount")?.Value ?? 0;
        
        // Obtener cliente
        var clienteRef = target.GetAttributeValue("customerid");
        var cliente = service.Retrieve(clienteRef.LogicalName, clienteRef.Id, 
            new ColumnSet("customerlevel"));
        
        // Usar el calculator inyectado
        var calculator = _calculatorFactory(service);
        decimal descuento = calculator.Calcular(cliente, monto);
        
        target["discountpercentage"] = descuento;
    }
}

El truco está en tener dos constructores. El constructor sin parámetros crea la implementación real (para producción). El constructor con parámetros permite inyectar cualquier implementación (para testing).

Testing con mocks

Ahora puedes escribir unit tests sin conectar a Dataverse:


[TestMethod]
public void TestDescuentoClienteGold()
{
    // Arrange: crear mock que siempre devuelve 10%
    var mockCalculator = new Mock();
    mockCalculator.Setup(c => c.Calcular(It.IsAny(), It.IsAny()))
                  .Returns(10m);
    
    // El plugin usará nuestro mock
    var plugin = new CalcularDescuentoPlugin(service => mockCalculator.Object);
    
    // Act: ejecutar con contexto simulado
    // ... configurar contexto mock y ejecutar
    
    // Assert: verificar resultado
    // ...
}

Creando una clase base reutilizable

Mucho del código de obtener servicios del IServiceProvider es repetitivo. Una clase base puede encapsularlo:


public abstract class PluginBase : IPlugin
{
    public void Execute(IServiceProvider serviceProvider)
    {
        var trace = (ITracingService)
            serviceProvider.GetService(typeof(ITracingService));
        var context = (IPluginExecutionContext)
            serviceProvider.GetService(typeof(IPluginExecutionContext));
        var factory = (IOrganizationServiceFactory)
            serviceProvider.GetService(typeof(IOrganizationServiceFactory));
        
        IOrganizationService service = factory.CreateOrganizationService(context.UserId);
        
        trace.Trace($"{GetType().Name}: Inicio");
        trace.Trace($"Mensaje: {context.MessageName}, Entidad: {context.PrimaryEntityName}");
        
        try
        {
            var ctx = new PluginContext
            {
                Context = context,
                Service = service,
                Trace = trace
            };
            
            ExecuteInternal(ctx);
            
            trace.Trace($"{GetType().Name}: Fin exitoso");
        }
        catch (InvalidPluginExecutionException)
        {
            throw;
        }
        catch (Exception ex)
        {
            trace.Trace($"Error: {ex.Message}");
            trace.Trace(ex.StackTrace);
            throw new InvalidPluginExecutionException(
                $"Error en {GetType().Name}: {ex.Message}", ex);
        }
    }
    
    protected abstract void ExecuteInternal(PluginContext ctx);
}

public class PluginContext
{
    public IPluginExecutionContext Context { get; set; }
    public IOrganizationService Service { get; set; }
    public ITracingService Trace { get; set; }
}

Ahora tus plugins son más limpios:


public class CalcularDescuentoPlugin : PluginBase
{
    protected override void ExecuteInternal(PluginContext ctx)
    {
        Entity target = (Entity)ctx.Context.InputParameters["Target"];
        
        // Tu lógica aquí, sin preocuparte por obtener servicios
        // o manejar excepciones generales
    }
}

Cuándo aplicar estos patrones

No todos los plugins necesitan esta estructura. Un plugin de 20 líneas que establece un campo calculado no justifica crear interfaces, clases separadas y factorías.

Considera usar estos patrones cuando:

  • El plugin tiene lógica de negocio compleja que cambia con el tiempo
  • La misma lógica se usa en múltiples plugins
  • Necesitas unit tests para la lógica
  • Trabajas en un equipo que mantiene el código a largo plazo

Puntos clave

  • Separa la lógica de negocio en clases independientes con interfaces
  • Usa constructores para permitir inyección de dependencias
  • El constructor sin parámetros crea implementaciones reales, el otro permite mocks
  • Una clase PluginBase reduce código repetitivo en todos los plugins
  • Aplica estos patrones en plugins complejos, no en todos

Para profundizar

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