ZebraPuma.Plugins
Système de Plugins Modulaire avec Chargement Dynamique
📋 Table des Matières
- Vue d'ensemble
- Architecture
- Interfaces et Classes
- PluginLoader
- Configuration
- Utilisation
- Exemples
- API Référence
- Bonnes Pratiques
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 :
- Intercepte les événements
AssemblyResolve - Recherche d'abord dans le répertoire du plugin demandeur
- Recherche ensuite dans tous les répertoires de plugins
- 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 :
- Vérifier
plugins.jsonexiste et est bien formé - Vérifier
AutoDiscoveresttrueou plugin est listé - Vérifier le chemin
FolderetAssembly - Vérifier que la classe implémente
IPlugin - Consulter les logs NLog
Problème : FileNotFoundException
Symptôme : Exception lors du chargement
Solutions :
- Vérifier que toutes les DLL sont présentes
- Copier les dépendances dans le dossier du plugin
- Utiliser Fusion Log Viewer pour diagnostiquer
- Vérifier les versions de .NET
Problème : InvalidCastException
Symptôme : Erreur de cast lors du chargement
Solutions :
- Vérifier que le plugin implémente l'interface demandée
- Vérifier la signature
where TPlugin : class, IPlugin - Reconstruire le plugin avec les bonnes références
Performance
Recommandations
- Cache des plugins : Ne pas recharger à chaque utilisation
- Lazy loading : Charger uniquement quand nécessaire
- Dispose : Toujours libérer les ressources
- 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 |