0

Plugin Architecture

Hoy vamos a hablar de algo que suena a magia negra pero que es el pan de cada día si usas VS Code, WordPress o incluso Docker: la Arquitectura de Plugins en .NET.

¿Alguna vez has deseado añadir una funcionalidad a tu aplicación sin tener que recompilar todo el proyecto, rezar tres Padrenuestros y desplegar 2GB de binarios? Si la respuesta es sí, quédate, porque vamos a aprender a «inyectar» código en tiempo de ejecución.

¡Manos a la obra!

Imagina que estás construyendo una consola de videojuegos. Tienes el hardware base (tu aplicación), pero no quieres soldar los juegos a la placa madre. Quieres que el usuario inserte un cartucho, la consola lo reconozca y ¡pum!, tienes un juego nuevo. En el mundo del software, los plugins son esos cartuchos que extienden tu «consola» sin que tengas que sacar el soldador (o el compilador) cada vez que sale un nivel nuevo.

¿Por qué importa?

Implementar una arquitectura de plugins no es solo por fardar de nivel senior. Se trata de:

  • Extensibilidad: Permitir que otros (o tú mismo) añadan funciones en caliente.
  • Aislamiento: Si un plugin falla, no tiene por qué tirar abajo toda la infraestructura (si lo haces bien).
  • Despliegue Independiente: Actualiza solo la lógica que cambió, no todo el monolito.

Veamoslo funcionando

Para este ejemplo, vamos a crear un sistema donde cargamos «Endpoints» dinámicamente usando un PDK (Plugin Development Kit).

El Contrato (PDK)

Primero, necesitamos una interfaz común que tanto el servidor como el plugin conozcan.

// En el proyecto Endpoint.PDK
public interface IPluginEndpoint 
{
    // El punto de entrada para nuestra lógica extendida
    Task ExecuteAsync(HttpContext context); 
}

[AttributeUsage(AttributeTargets.Class)]
public class PathAttribute : Attribute 
{
    public string Path { get; }
    public PathAttribute(string path) => Path = path;
}

El Plugin (El Cartucho)

El plugin referencia al PDK, pero ¡ojo!, usa PrivateAssets=»all» en la referencia para evitar colisiones de DLLs en el servidor.

[Path("/saludo-ninja")]
public class NinjaEndpoint : IPluginEndpoint 
{
    public async Task ExecuteAsync(HttpContext context) 
    {
        // Comentario divertido: El ninja no se ve, pero el código se siente.
        await context.Response.WriteAsync("¡Kiai! Plugin cargado y funcionando.");
    }
}

La Carga Mágica (Host)

Aquí es donde usamos el AssemblyLoadContext. No uses Assembly.LoadFrom directamente en producción si quieres poder descargar el plugin después.

// Usamos un contexto coleccionable para poder "quemar" el plugin si hace falta
[cite_start]var loadContext = new AssemblyLoadContext("NinjaScope", isCollectible: true); [cite: 15, 16]
[cite_start]var assembly = loadContext.LoadFromAssemblyPath(@"C:\plugins\NinjaPlugin.dll"); [cite: 16]

// Buscamos la clase que implementa nuestra interfaz
[cite_start]var type = assembly.GetTypes().FirstOrDefault(t => typeof(IPluginEndpoint).IsAssignableFrom(t)); [cite: 10]

if (type != null) 
{
    [cite_start]var instance = (IPluginEndpoint)Activator.CreateInstance(type)!; [cite: 12]
    // ¡Listo para usar!
}

Resultados

Cargar DLLs dinámicamente es emocionante hasta que te das cuenta de que la memoria en .NET es como ese amigo que se queda en tu sofá y no se quiere ir.

  • Pro: Es la máxima expresión de desacoplamiento.
  • Contra: El GC (Garbage Collector) es caprichoso. Si guardas una referencia al tipo del plugin en una variable estática del host, el plugin JAMÁS se descargará de la memoria, aunque llames a Unload().
  • Peligro oculto: Librerías como System.Text.Json cachean tipos. Si serializas un objeto del plugin, esa referencia puede quedar atrapada en el cache global del host, causando una fuga de memoria (Memory Leak).

Buenas Prácticas

  1. Aísla con Métodos: Ejecuta la carga en un método separado con [MethodImpl(MethodImplOptions.NoInlining)]. Esto asegura que las referencias locales salgan del stack y el GC pueda limpiar el plugin.
  2. Usa WeakReferences: Si quieres verificar si un plugin se descargó de verdad, usa un WeakReference. Si IsAlive es falso después de un GC.Collect(), ¡lo lograste!.
  3. Pura Funcionalidad: Intenta que tus plugins sean lo más «puros» posible. Evita que toquen estados globales del host.

Conclusión

La arquitectura de plugins es como tener un garage lleno de herramientas: es genial hasta que te das cuenta de que tienes 50 llaves inglesas diferentes y no sabes cuál es la que no gotea.

Reto: Intenta implementar un FileSystemWatcher que detecte cuando dejas caer una DLL en una carpeta y la cargue automáticamente. Si logras que se descargue la versión vieja sin reiniciar el servidor, oficialmente eres el mago de los binarios.

Fernando Sonego

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *