Table of Contents

ZebraPuma.Plugins

Système de Plugins Modulaire avec Chargement Dynamique


📋 Table des Matières


Vue d'ensemble

ZebraPuma.Plugins est une bibliothèque de base pour créer des applications modulaires et extensibles en .NET. Elle fournit un système de plugins robuste avec chargement dynamique, auto-découverte, et gestion du cycle de vie.

Caractéristiques Principales

  • Chargement dynamique de plugins depuis DLL
  • Auto-découverte des plugins dans les dossiers configurés
  • Configuration JSON flexible (plugins.json)
  • Résolution automatique des dépendances d'assemblies
  • Gestion du cycle de vie complet (Initialize, Dispose)
  • Support multi-frameworks (.NET Framework 4.8 et .NET 10.0)
  • Pattern Dispose intégré pour la gestion des ressources
  • Contexte d'initialisation avec propriétés personnalisables

Cibles Supportées

  • .NET Framework 4.8
  • .NET 10.0

Dépendances

  • Newtonsoft.Json 13.0.4
  • NLog 6.0.7

Architecture

Diagramme de Composants

┌─────────────────────────────────────────────────────────┐
│                   Application Host                       │
│                                                           │
│  ┌────────────────────────────────────────────────────┐  │
│  │              PluginLoader                          │  │
│  │  • LoadPlugins<TPlugin>()                         │  │
│  │  • Configuration (plugins.json)                    │  │
│  │  • Auto-discovery                                  │  │
│  │  • Assembly Resolver                               │  │
│  └────────────────────────────────────────────────────┘  │
│                          │                               │
│                          ▼                               │
│  ┌────────────────────────────────────────────────────┐  │
│  │           IPluginContext                           │  │
│  │  • BaseDirectory                                   │  │
│  │  • PluginDirectory                                 │  │
│  │  • Properties (Dictionary)                         │  │
│  └────────────────────────────────────────────────────┘  │
│                          │                               │
└──────────────────────────┼───────────────────────────────┘
                           │
         ┌─────────────────┴─────────────────┐
         │                                   │
         ▼                                   ▼
┌─────────────────┐              ┌─────────────────┐
│  Plugin A       │              │  Plugin B       │
│  • IPlugin      │              │  • IPlugin      │
│  • Initialize() │              │  • Initialize() │
│  • Dispose()    │              │  • Dispose()    │
└─────────────────┘              └─────────────────┘

Flux de Chargement

1. Application démarre
        ↓
2. Appel LoadPlugins<T>()
        ↓
3. Lecture plugins.json (si existe)
        ↓
4. Auto-discovery (si activé)
        ↓
5. Résolution des chemins et assemblies
        ↓
6. Activation PluginAssemblyResolver
        ↓
7. Chargement Assembly.LoadFrom()
        ↓
8. Création instances Activator.CreateInstance()
        ↓
9. Appel Initialize(context) sur chaque plugin
        ↓
10. Retour des plugins chargés

Interfaces et Classes

IPlugin

Interface de base que tous les plugins doivent implémenter.

public interface IPlugin : IDisposable
{
    /// <summary>
    /// Nom unique du plugin.
    /// </summary>
    string Name { get; }

    /// <summary>
    /// Version du plugin (ex: "1.0.0").
    /// </summary>
    string Version { get; }

    /// <summary>
    /// Description optionnelle du plugin.
    /// </summary>
    string Description { get; }

    /// <summary>
    /// Initialise le plugin après son chargement.
    /// </summary>
    void Initialize(IPluginContext context);
}

IPluginContext

Contexte fourni lors de l'initialisation du plugin.

public interface IPluginContext
{
    /// <summary>
    /// Répertoire de base de l'application.
    /// </summary>
    string BaseDirectory { get; }

    /// <summary>
    /// Répertoire du plugin spécifique.
    /// </summary>
    string PluginDirectory { get; }

    /// <summary>
    /// Propriétés personnalisées (dictionnaire clé-valeur).
    /// </summary>
    IDictionary<string, object> Properties { get; }
}

PluginBase

Classe de base abstraite facilitant l'implémentation de plugins avec pattern Dispose.

public abstract class PluginBase : IPlugin
{
    public abstract string Name { get; }
    
    public virtual string Version => 
        GetType().Assembly.GetName().Version?.ToString() ?? "1.0.0";
    
    public virtual string Description => string.Empty;
    
    protected IPluginContext Context { get; private set; }
    
    public virtual void Initialize(IPluginContext context)
    {
        Context = context;
    }
    
    protected virtual void DisposeManagedResources() { }
    
    protected virtual void DisposeUnmanagedResources() { }
    
    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }
    
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed)
        {
            if (disposing)
            {
                DisposeManagedResources();
            }
            DisposeUnmanagedResources();
            _disposed = true;
        }
    }
}

PluginLoader

La classe statique PluginLoader est le point d'entrée principal pour charger les plugins.

Méthode Principale

public static IReadOnlyList<TPlugin> LoadPlugins<TPlugin>(
    PluginLoadingOptions options = null
) where TPlugin : class, IPlugin

Options de Chargement

public sealed class PluginLoadingOptions
{
    /// <summary>
    /// Répertoire de base. Défaut: AppDomain.CurrentDomain.BaseDirectory
    /// </summary>
    public string BaseDirectory { get; set; }
    
    /// <summary>
    /// Nom du dossier contenant les plugins. Défaut: "Plugins"
    /// </summary>
    public string PluginsDirectoryName { get; set; } = "Plugins";
    
    /// <summary>
    /// Nom du fichier de configuration. Défaut: "plugins.json"
    /// </summary>
    public string ConfigFileName { get; set; } = "plugins.json";
    
    /// <summary>
    /// Override du flag AutoDiscover du fichier JSON
    /// </summary>
    public bool? AutoDiscoverOverride { get; set; }
}

PluginConfigLoader

Utilitaire pour charger la configuration JSON d'un plugin.

public static class PluginConfigLoader
{
    /// <summary>
    /// Charge un fichier JSON nommé d'après l'assembly (en minuscule).
    /// Exemple: MonPlugin.dll -> monplugin.json
    /// </summary>
    public static TConfig LoadAssemblyConfig<TConfig>()
}

PluginAssemblyResolver

Résout automatiquement les dépendances d'assemblies des plugins.

Fonctionnement :

  1. Intercepte les événements AssemblyResolve
  2. Recherche d'abord dans le répertoire du plugin demandeur
  3. Recherche ensuite dans tous les répertoires de plugins
  4. Charge l'assembly avec Assembly.LoadFrom()

Configuration

Structure du fichier plugins.json

{
  "AutoDiscover": true,
  "Plugins": [
    {
      "Folder": "MonPlugin",
      "Assembly": "MonPlugin.dll",
      "Type": "MonNamespace.MonPlugin"
    },
    {
      "Folder": "AutrePlugin",
      "Assembly": "AutrePlugin.dll",
      "Type": "AutreNamespace.AutrePlugin"
    }
  ]
}

Propriétés

Propriété Type Description
AutoDiscover bool Active la découverte automatique des plugins dans les sous-dossiers
Plugins array Liste des plugins à charger explicitement
Plugins[].Folder string Nom du sous-dossier dans le répertoire Plugins/
Plugins[].Assembly string Nom du fichier DLL
Plugins[].Type string Nom complet du type (Namespace.Classe)

Modes de Chargement

Mode 1 : Configuration Explicite Uniquement

{
  "AutoDiscover": false,
  "Plugins": [ ... ]
}

Charge uniquement les plugins listés.

Mode 2 : Auto-Discovery Uniquement

{
  "AutoDiscover": true
}

Scanne automatiquement tous les sous-dossiers de Plugins/.

Mode 3 : Mixte (Recommandé)

{
  "AutoDiscover": true,
  "Plugins": [ ... ]
}

Charge les plugins listés ET scanne pour d'autres plugins.


Utilisation

Scénario 1 : Plugin Simple

Étape 1 : Créer le Plugin

using ZebraPuma.Plugins;

namespace MonApp.Plugins
{
    public class MonPlugin : PluginBase
    {
        public override string Name => "MonPlugin";
        public override string Description => "Un plugin d'exemple";

        public override void Initialize(IPluginContext context)
        {
            base.Initialize(context);
            Console.WriteLine($"Plugin initialisé dans {context.PluginDirectory}");
        }

        protected override void DisposeManagedResources()
        {
            Console.WriteLine("Nettoyage des ressources");
        }
    }
}

Étape 2 : Compiler et Déployer

# Compiler le plugin
dotnet build MonPlugin.csproj

# Créer le dossier de déploiement
mkdir AppHost\Plugins\MonPlugin

# Copier la DLL
copy bin\Debug\net10.0\MonPlugin.dll AppHost\Plugins\MonPlugin\

Étape 3 : Configurer (optionnel)

Créer AppHost/plugins.json :

{
  "AutoDiscover": true
}

Étape 4 : Charger dans l'Application

using ZebraPuma.Plugins;

class Program
{
    static void Main()
    {
        var plugins = PluginLoader.LoadPlugins<IPlugin>();
        
        foreach (var plugin in plugins)
        {
            Console.WriteLine($"Plugin: {plugin.Name} v{plugin.Version}");
            Console.WriteLine($"Description: {plugin.Description}");
        }

        // Cleanup
        foreach (var plugin in plugins)
        {
            plugin.Dispose();
        }
    }
}

Scénario 2 : Plugin avec Configuration

Configuration du Plugin (monplugin.json)

{
  "DatabaseConnection": "Server=localhost;Database=test;",
  "MaxRetries": 3,
  "EnableLogging": true
}

Classe de Configuration

public class MonPluginConfig
{
    public string DatabaseConnection { get; set; }
    public int MaxRetries { get; set; }
    public bool EnableLogging { get; set; }
}

Plugin avec Configuration

public class MonPlugin : PluginBase
{
    private MonPluginConfig _config;

    public override string Name => "MonPlugin";

    public override void Initialize(IPluginContext context)
    {
        base.Initialize(context);
        
        // Charger la configuration
        _config = PluginConfigLoader.LoadAssemblyConfig<MonPluginConfig>();
        
        if (_config.EnableLogging)
        {
            Console.WriteLine($"Connexion: {_config.DatabaseConnection}");
        }
    }
}

Scénario 3 : Interface de Plugin Personnalisée

// Définir une interface spécialisée
public interface IDataProcessor : IPlugin
{
    void ProcessData(string input);
    string GetResult();
}

// Implémenter l'interface
public class CsvProcessor : PluginBase, IDataProcessor
{
    private string _result;

    public override string Name => "CsvProcessor";

    public void ProcessData(string input)
    {
        _result = $"Processed: {input}";
    }

    public string GetResult() => _result;
}

// Charger uniquement les plugins de ce type
var processors = PluginLoader.LoadPlugins<IDataProcessor>();
foreach (var processor in processors)
{
    processor.ProcessData("test.csv");
    Console.WriteLine(processor.GetResult());
}

Exemples

Exemple 1 : Application Console Multi-Plugins

using System;
using System.Collections.Generic;
using ZebraPuma.Plugins;

namespace PluginHost
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("=== Plugin Host ===\n");

            var options = new PluginLoader.PluginLoadingOptions
            {
                PluginsDirectoryName = "Plugins",
                ConfigFileName = "plugins.json",
                AutoDiscoverOverride = true
            };

            var plugins = PluginLoader.LoadPlugins<IPlugin>(options);

            Console.WriteLine($"Chargé {plugins.Count} plugin(s):\n");

            foreach (var plugin in plugins)
            {
                Console.WriteLine($"  • {plugin.Name}");
                Console.WriteLine($"    Version: {plugin.Version}");
                Console.WriteLine($"    Description: {plugin.Description}\n");
            }

            Console.WriteLine("Appuyez sur une touche pour terminer...");
            Console.ReadKey();

            // Cleanup
            foreach (var plugin in plugins)
            {
                plugin.Dispose();
            }
        }
    }
}

Exemple 2 : Plugin avec Logging NLog

using NLog;
using ZebraPuma.Plugins;

public class LoggingPlugin : PluginBase
{
    private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

    public override string Name => "LoggingPlugin";

    public override void Initialize(IPluginContext context)
    {
        base.Initialize(context);
        
        Logger.Info("Plugin {Name} initialisé", Name);
        Logger.Debug("BaseDirectory: {Dir}", context.BaseDirectory);
        Logger.Debug("PluginDirectory: {Dir}", context.PluginDirectory);
    }

    protected override void DisposeManagedResources()
    {
        Logger.Info("Plugin {Name} en cours de fermeture", Name);
    }
}

Exemple 3 : Structure de Projet Complète

MonApplication/
├── MonApplication.csproj
├── Program.cs
├── plugins.json
├── NLog.config
├── Plugins/
│   ├── PluginA/
│   │   ├── PluginA.dll
│   │   ├── PluginA.pdb
│   │   ├── plugina.json
│   │   └── dependance.dll
│   ├── PluginB/
│   │   ├── PluginB.dll
│   │   ├── PluginB.pdb
│   │   └── pluginb.json
│   └── PluginC/
│       └── PluginC.dll
└── logs/
    └── {date}.log

plugins.json

{
  "AutoDiscover": true,
  "Plugins": [
    {
      "Folder": "PluginA",
      "Assembly": "PluginA.dll",
      "Type": "MonApp.Plugins.PluginA"
    }
  ]
}

API Référence

Namespace: ZebraPuma.Plugins

Classes

Classe Description
PluginBase Classe de base abstraite pour plugins
PluginLoader Chargeur de plugins statique
PluginConfigLoader Utilitaire pour charger config JSON

Interfaces

Interface Description
IPlugin Contrat de base pour tous les plugins
IPluginContext Contexte d'initialisation

Types

Type Description
PluginLoadingOptions Options de chargement de plugins

Bonnes Pratiques

1. Nommage et Versioning

Bon

public class DataExportPlugin : PluginBase
{
    public override string Name => "DataExportPlugin";
    public override string Version => "2.1.0";
    public override string Description => "Exporte les données au format CSV et JSON";
}

Mauvais

public class Plugin1 : PluginBase
{
    public override string Name => "p1";
    public override string Version => "1";
}

2. Gestion des Ressources

Bon

public class DatabasePlugin : PluginBase
{
    private SqlConnection _connection;

    public override void Initialize(IPluginContext context)
    {
        base.Initialize(context);
        _connection = new SqlConnection(connectionString);
        _connection.Open();
    }

    protected override void DisposeManagedResources()
    {
        _connection?.Close();
        _connection?.Dispose();
    }
}

Mauvais (fuite de ressources)

public class DatabasePlugin : PluginBase
{
    private SqlConnection _connection;

    public override void Initialize(IPluginContext context)
    {
        _connection = new SqlConnection(connectionString);
        _connection.Open();
        // Pas de Dispose!
    }
}

3. Gestion d'Erreurs

Bon

public override void Initialize(IPluginContext context)
{
    try
    {
        base.Initialize(context);
        
        var config = PluginConfigLoader.LoadAssemblyConfig<Config>();
        // ... initialisation
    }
    catch (FileNotFoundException ex)
    {
        Logger.Error(ex, "Fichier de configuration introuvable");
        throw new PluginInitializationException("Config manquante", ex);
    }
    catch (Exception ex)
    {
        Logger.Fatal(ex, "Erreur critique d'initialisation");
        throw;
    }
}

4. Configuration

Bon - Configuration typée

public class MyPluginConfig
{
    [JsonProperty("connectionString")]
    public string ConnectionString { get; set; }
    
    [JsonProperty("timeout")]
    public int Timeout { get; set; } = 30;
}

var config = PluginConfigLoader.LoadAssemblyConfig<MyPluginConfig>();

Mauvais - Configuration en dur

const string connectionString = "Server=localhost;...";

5. Dépendances

Bon - Dépendances dans le dossier du plugin

Plugins/
  MonPlugin/
    MonPlugin.dll
    Newtonsoft.Json.dll
    NLog.dll

Mauvais - Dépendances manquantes

Plugins/
  MonPlugin/
    MonPlugin.dll
    // Newtonsoft.Json.dll manquant -> Crash!

6. Thread Safety

Bon

public class ThreadSafePlugin : PluginBase
{
    private readonly object _lock = new object();
    private int _counter;

    public void IncrementCounter()
    {
        lock (_lock)
        {
            _counter++;
        }
    }
}

7. Logging

Bon

private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

public override void Initialize(IPluginContext context)
{
    Logger.Info("Initialisation de {Plugin} v{Version}", Name, Version);
    Logger.Debug("Context: {@Context}", context);
    
    try
    {
        // ... code
        Logger.Info("Initialisation réussie");
    }
    catch (Exception ex)
    {
        Logger.Error(ex, "Échec d'initialisation");
        throw;
    }
}

Troubleshooting

Problème : Plugin Non Chargé

Symptôme : Le plugin n'apparaît pas dans la liste

Solutions :

  1. Vérifier plugins.json existe et est bien formé
  2. Vérifier AutoDiscover est true ou plugin est listé
  3. Vérifier le chemin Folder et Assembly
  4. Vérifier que la classe implémente IPlugin
  5. Consulter les logs NLog

Problème : FileNotFoundException

Symptôme : Exception lors du chargement

Solutions :

  1. Vérifier que toutes les DLL sont présentes
  2. Copier les dépendances dans le dossier du plugin
  3. Utiliser Fusion Log Viewer pour diagnostiquer
  4. Vérifier les versions de .NET

Problème : InvalidCastException

Symptôme : Erreur de cast lors du chargement

Solutions :

  1. Vérifier que le plugin implémente l'interface demandée
  2. Vérifier la signature where TPlugin : class, IPlugin
  3. Reconstruire le plugin avec les bonnes références

Performance

Recommandations

  1. Cache des plugins : Ne pas recharger à chaque utilisation
  2. Lazy loading : Charger uniquement quand nécessaire
  3. Dispose : Toujours libérer les ressources
  4. Assembly resolver : Dispose du resolver quand terminé

Métriques Typiques

Opération Temps Moyen
Chargement 1 plugin 50-100 ms
Chargement 10 plugins 300-500 ms
Initialize d'un plugin 10-50 ms

⬅️ Retour à la documentation principale