Aller au contenu

Ajouter un plugin

Gabarit

src/Schuly.Plugin.Example/ - celui que tu copies - est volontairement minimal :

src/Schuly.Plugin.Example/
├── ExamplePlugin.cs                  # implémentation de ISchulyPlugin
├── Schuly.Plugin.Example.csproj
└── Schuly.Plugin.Example.slnx        # ouvre ce plugin de manière autonome dans un IDE

Les vrais plugins gagnent au fil du temps un dossier Controllers/, Data/ (un DbContext + des migrations EF) et Services/ selon leurs besoins - regarde src/Schuly.Plugin.Schulware/ ou src/Schuly.Plugin.OdaOrg/ pour voir à quoi ça ressemble une fois qu'un plugin dialogue avec une base de données et une API externe. Rien de tout cela n'est requis pour démarrer.

  1. Copie src/Schuly.Plugin.Example/ vers src/Schuly.Plugin.<Name>/ :

    src/Schuly.Plugin.<Name>/
    ├── <Name>Plugin.cs
    ├── Schuly.Plugin.<Name>.csproj
    └── Schuly.Plugin.<Name>.slnx
  2. Renomme ExamplePlugin.cs<Name>Plugin.cs ; renomme la classe et le namespace en conséquence. La classe implémente ISchulyPlugin.

  3. Renomme le .csproj (et le .slnx) en Schuly.Plugin.<Name>. Conserve <TargetFramework>net10.0</TargetFramework> et la PackageReference vers Schuly.Plugin.Abstractions. Renseigne <Version>, <Description>, <Authors> - ces informations alimentent l'index de distribution publié.

  4. Ouvre une issue étiquetée new-plugin, puis suis le processus de contribution : branche → PR (Closes #<issue>) → squash-merge.

Aucun changement de workflow n'est nécessaire - build_push.yml détecte automatiquement tout src/Schuly.Plugin.*/*.csproj. Voir setup/distribution.md.

Le cycle de vie ISchulyPlugin

La classe du plugin est la racine de composition (compacte, comme un Program.cs). L'hôte de plugins du backend appelle les méthodes suivantes dans cet ordre au démarrage :

ConfigureServices(IServiceCollection services, PluginServiceContext context)

Enregistre ici tes services, options, la tâche en arrière-plan et le login. context expose :

  • context.ConnectionString - la chaîne de connexion Postgres dédiée au plugin (l'hôte la modifie pour cibler schuly_plugin_<name> ; voir migrations.md).
  • context.Configuration - la configuration YAML du plugin (Schuly.Plugin.<Name>.yml).

Enregistrements typiques (tirés de Schulware/OdaOrg) :

csharp
services.AddDbContext<MyDbContext>(o => o.UseNpgsql(context.ConnectionString));

services.AddSingleton<MySyncTask>();
services.AddSingleton<IPluginBackgroundTask>(sp => sp.GetRequiredService<MySyncTask>());

// Coffre-fort de secrets en mémoire, isolé par plugin, indexé par le nom du plugin par l'hôte.
services.AddScoped(sp => new MySecretStore(
    sp.GetRequiredKeyedService<IPluginVault>(MyPlugin.PluginName)));

services.AddScoped<IPluginLogin, MyLogin>();

La clé du coffre-fort doit être une constante (utilisée avec [FromKeyedServices(PluginName)]), expose donc un public const string PluginName. Le coffre-fort n'existe qu'en mémoire - les secrets ne survivent pas à un redémarrage du backend, et le code de synchronisation doit gérer le cas d'un coffre vide ("reconnexion nécessaire").

ConfigureEndpoints(IEndpointRouteBuilder endpoints)

Mappe ici les routes en API minimale. Le plugin Example l'utilise directement :

csharp
endpoints.MapGet("/api/plugins/example/hello",
    (IPluginUserContext userContext) => Results.Ok(...)).RequireAuthorization();
endpoints.MapGet("/api/plugins/example/info", () => Results.Ok(...)).AllowAnonymous();

Les plugins Schulware et OdaOrg laissent cette méthode vide et placent plutôt leurs routes dans Controllers/ sous forme de contrôleurs ASP.NET MVC - l'hôte enregistre l'assembly du plugin comme ApplicationPart MVC, donc les contrôleurs [ApiController] sont découverts automatiquement. Les deux approches fonctionnent ; les contrôleurs passent mieux à l'échelle pour des surfaces plus vastes.

MigrateAsync(IServiceProvider serviceProvider, CancellationToken)

Applique les migrations EF Core. Résous le DbContext depuis un scope et appelle db.Database.MigrateAsync(). Le plugin Example (sans base de données) retourne simplement Task.CompletedTask. Voir migrations.md - utilise MigrateAsync(), jamais EnsureCreatedAsync.

IPluginBackgroundTask (optionnel)

Travail récurrent. Implémente Name, Interval et ExecuteAsync. Le PluginBackgroundTaskHost du backend invoque ExecuteAsync à chaque tick d'Interval :

csharp
public class MySyncTask : IPluginBackgroundTask
{
    public string Name => "My Data Sync";
    public TimeSpan Interval => TimeSpan.FromMinutes(30);

    public async Task ExecuteAsync(IServiceProvider serviceProvider, CancellationToken ct)
    {
        using var scope = serviceProvider.CreateScope();
        // résoudre les services à portée limitée, effectuer le travail
    }
}