J'ai toujours été impréssionné par des applications comme Prestashop ou Matomo qui proposent une multitude de plugins (commerciaux ou pas) afin d'améliorer une application initiale et l'etendre à l'infini.
L'architecture PHP est basée sur un language interprété, alors d'une certaine manière c'est "facile" d'ajouter un script en se conformant à une api pour tenter d'etendre ou ajouter une fonctionnalité. on récupère un ou des fichiers à la volée, on les place au bon endroit, et c'est prêt à être utilisé. Franchement c'est très fort sur ce point. En revanche sur la sécurité c'est très limite.
Qu'en est-il avec aspnet blazor ?
Je ne vais pas m'etendre sur dotnet 4x ou l'injection de dépendance n'était pas "by-design" il fallait utiliser des librairies comme Unity (pas celle qui fait de la 3d) ou NInject, il y a quelques années, sur ce principe, j'ai mis en place pas mal d'architectures avec des plugins et c'était assez flexible.
Non je vais parler ici d'Asp.Net Blazor et son modèle d'injection qui n'est pas vraiment prévu pour les plugins, en tout cas pas pour ce qui est écrit à la fin de cet article.
Le principe de mettre en place des plugins avec Blazor Serveur, c'est pouvoir par exemple ajouter un menu, ajouter des composants ou les remplacer, ça demande une préparation initiale pour les "accueillir".
Spoiler : Faire ça à la volée comme PHP pour quelque chose d'un peut évolué est quasiment impossible sauf cas ultra simple.
Les fonctionnalités qui doivent être prise en charge :
- Avoir un système de versionning pour les mises à jour
- Ajouter des items de menu dans une sidebar générale
- Ajouter des boutons d'action dans les toolbars de chaque écran
- Ajouter des colonnes dans les grilles existantes
- Changer ou remplacer un composant de base (par exemple un écran de login)
- Ajouter ses propres données dans une base de données centrale ou une base dédiée
- Ajouter ses propres routes pour afficher de nouveaux écrans
- On doit pouvoir ajouter ou retirer les plugins
Architecture des diffents projets qui composent le système
Une assembly PluginClient , c'est elle qui va permettre le bootstraping :
- Vérifie au démarrage les plugins qui sont candidats à la suppression et les supprime s'il y en a.
- Vérifie s'il y a des nouveaux plugins à installer, si c'est le cas les place dans un répertoire de plugins dédié.
- Vérifie si les plugins installés sont compatibles avec l'application en cours, et si ce n'est pas le cas les supprime.
- Configure le service d'update pour pouvoir aller chercher les mise à jour dans un magasin de plugins.
- Charge les plugins en passant le builder d'application web en paramètre afin que chacun inscrive leurs services et autres, si le chargement se passe mal, supprime le plugin "defectueux".
- Utilise les plugins en passant en paramètre l'application buildée afin d'inscrire par exemple des routes pour de nouveaux composants.
- Affiche un écran de gestion des plugins qui communique avec le magasin de plugins afin de montrer les plugins installés, les mise à jour possibles et la possibilité d'ajouter ou supprimer.
sequenceDiagram
autonumber
actor Host as Process/Host
participant Program as Program.cs (Startup)
participant PM as PluginManager
participant FS as FileSystem (PluginsDir)
participant Store as PluginStore (Repository)
participant Upd as UpdateService
participant Builder as WebApplicationBuilder
participant App as WebApplication
Host->>Program: Start
Program->>PM: Initialize(pluginsDir, policies)
PM->>FS: EnsureDirectoryExists(PluginsDir)
%% 1) Supprimer les plugins candidats à la suppression
Program->>PM: CleanupCandidates()
PM->>FS: ListPluginsMarkedForRemoval()
alt candidates found
loop for each candidate
PM->>FS: DeletePlugin(candidate)
end
else none
PM-->>Program: No removal candidates
end
%% 2) Installer de nouveaux plugins
Program->>PM: InstallNewPluginsIfAny()
PM->>Store: CheckNewPlugins(manifest/version/channel)
alt new plugins available
loop for each new plugin
Store-->>PM: DownloadPackage(plugin)
PM->>FS: PlaceInPluginsDir(pluginPackage)
PM->>FS: Extract/Deploy(plugin)
end
else none
PM-->>Program: No new plugins
end
%% 3) Vérifier la compatibilité et supprimer ceux incompatibles
Program->>PM: ValidateCompatibility(appVersion/runtime)
PM->>FS: EnumerateInstalledPlugins()
loop for each installed plugin
PM->>PM: CheckCompatibility(plugin, appVersion, runtime)
alt incompatible
PM->>FS: DeletePlugin(plugin)
else compatible
PM-->>Program: Keep plugin
end
end
%% 4) Configurer le service d'update
Program->>Upd: Configure(pluginStoreEndpoint, credentials, policies)
Upd->>Store: WarmUp/HealthCheck()
Store-->>Upd: OK
%% 5) Charger les plugins (registration via builder) + suppression si défaut
Program->>Builder: CreateBuilder(args)
Program->>PM: LoadPlugins(builder)
PM->>FS: EnumerateInstalledPlugins()
loop for each plugin
PM->>PM: LoadAssembly(plugin)
PM->>PM: Resolve IPluginStartup
PM->>PM: plugin.ConfigureServices(builder)
alt load/configure failed
PM->>FS: DeletePlugin(plugin)
PM-->>Program: Plugin removed as defective
else success
PM-->>Program: Plugin loaded
end
end
%% 6) Utiliser les plugins avec l'app buildée (routes, middleware, etc.)
Program->>App: Build()
Program->>PM: UsePlugins(app)
PM->>FS: EnumerateLoadedPlugins()
loop for each loaded plugin
PM->>PM: plugin.Configure(app)
note right of PM: Ex: MapGroup/MapGet, middleware,\nendpoints, static files, etc.
end
Program->>App: Run()Une web application de type Api PluginApi, c'est le magasin de plugins :
- Upload d'un package composé d'une assembly dll + pdb + manifest qui doivent implémenter à minima une interface IPlugin.
- Gestion des backups des plugins pour éventuel rollback en cas de gros bug.
- Suppression des plugins qui ne servent plus.
Un générateur de code PluginManifestGenerator que chaque Plugin doit référencer pour que celui-ci génère au build un fichier manifest.json à partir d'attribut [AssemblyMetadata], afin de recuperer :
- Le numéro de version
- La version minimale de l'application
- L'id , le nom, la description
- Et toutes les metadatas "cosmétiques" bannerurl, logourl, ect...
Une application console PluginPublisher dont le role est de builer, packager et publier le plugin soit vers le magasin de plugins (en production) , soit directement dans le répertoire des plugins de l'application en mode (développement). Le fichier manifest.json ainsi que la dll et son pdb sont inclus dans le package (.zip)
Et enfin une application console PluginApiCli dont le role est la gestion à distance du magasin de plugin :
- Listage des plugins.
- Suppression d'un plugin.
- Lancer un rollback sur une version en cas de problème.
Diagramme des differents composants
flowchart LR
%% =========================
%% Architecture Plugins Blazor
%% =========================
subgraph APP["Application Web Blazor (Host)"]
Program["Program.cs / Startup"]
Builder["WebApplicationBuilder"]
App["WebApplication (buildée)"]
subgraph PC["PluginClient (assembly de bootstrap)"]
PM["PluginManager"]
Upd["UpdateService (client)"]
UI["UI Gestion plugins\n(installed / updates / delete)"]
end
PluginsDir[("Répertoire Plugins\n(dll + pdb + manifest.json)")]
end
subgraph STORE["Magasin de plugins"]
Api["PluginApi (Web API)\nUpload/Backup/Rollback/Suppression"]
Storage[("Stockage packages\n.zip + versions + backups")]
end
subgraph TOOLING["Tooling & Build plugins"]
Gen["PluginManifestGenerator\n(Source Generator)\n=> manifest.json"]
Pub["PluginPublisher (console)\nBuild/Package/Publish"]
Cli["PluginApiCli (console)\nAdmin distant (list/delete/rollback)"]
end
subgraph PLUGINS["Plugins (1..n)"]
Pkg["Package Plugin (.zip)\n- plugin.dll\n- plugin.pdb\n- manifest.json"]
Impl["Assembly Plugin\nimplémente IPlugin (+ Startup)\n- ConfigureServices(builder)\n- Configure(app)"]
end
%% --- Relations internes à l'app ---
Program --> PC
Program --> Builder
Builder --> App
PC --> PM
PC --> Upd
PC --> UI
PM <--> PluginsDir
PM -->|"Load via AssemblyLoadContext\n(avant Build)"| Builder
PM -->|"Use / Routes / Endpoints\n(après Build)"| App
%% --- Store / Update / UI ---
Upd <--> Api
UI <--> Api
Api <--> Storage
%% --- Publication / Administration ---
Gen --> Pkg
Impl --> Pkg
Pub --> Pkg
Pub --> Api
Pub --> PluginsDir
Cli <--> Api
%% --- Installation côté app ---
Api -->|"Download plugin package"| Upd
Upd -->|"Deploy / Extract"| PluginsDir
PluginsDir -->|"Assemblies disponibles"| PMRestarter l'application
Il nous reste un dernier problème à résoudre :
- Comment stopper une application d'elle même ?
- Comment la faire repartir ?
Pour le point numéro 1 c'est assez facile, à partir d'un click sur bouton on lance ceci :
@inject IHostApplicationLifetime HostApplicationLifetime
@code {
async StopApplication()
{
// TODO : Ici la demande de confirmation
HostApplicationLifetime.StopApplication()
}
}
<button type="btn btn-danger" @click="StopApplication">Redemarrer</button>
Pour le point numéro 2 c'est la que ça se corse, il va falloir trouver une solution de redémarrage car si le framework à prévu l'arrêt, il n'a pas prévu le redemarrage.
En ce qui concerne Appliman j'ai développé une solution interne de déploiement dont le nom de code est Clustiis, elle détecte que le site est stoppé et le redémarre automatiquement s'il est configuré en auto-start.
Sans un dispositif comme Clustiis, il va falloir en fonction du host Windows IIS ou Linux trouver d'autres solutions :
sur linux si le site est configuré comme un service, alors dans le fichier de configuration il faut ajouter cette ligne :
[Unit]
Description=MyApplication
After=network.target
[Service]
WorkingDirectory=/opt/wwwroot/myappli
ExecStart=/usr/bin/dotnet /opt/wwwroot/myappli/myappli.dll --urls http://*:1234
User=myuser
Group=mygroup
# Ici la configuraiton pour un restart automatique
Restart=always
RestartSec=5
KillSignal=SIGINT
SyslogIdentifier=MyApplication
[Install]
WantedBy=multi-user.target
Avec Docker c'est facile, il faut ajouter cette ligne dans docker-compose :
restart: always
Avec IIS malheureusement il va falloir bidouiller, comme par exemple modifier au moment du stop le fichier "web.config" ce qui entraine un recycle du worker process, je sais, c'est très sale :( , de toute façon c'est ma nouvelle résolution, je ne bosse plus que sur linux.
Nous avons mis en place ce mécanisme pour une application que nous sommes en train de developper AudiStock (qui sortira en ce début d'année) dont le but est d'optimisation de stock basée sur des patterns de comportement d'achat/vente.
Il y a des plugins qui font de la classification (method ABC et XYZ)
Des plugins qui font du profiling (Rupures fréquentes, Saisonnalité, ect..)
Un plugin qui sert de serveur MCP
On peut via ces Plugins, étendre l'application à l'infini.