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"| PM

Restarter 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.