Aggiungere un plugin
Scheletro
src/Schuly.Plugin.Example/ - quello che copi - è volutamente minimale:
src/Schuly.Plugin.Example/
├── ExamplePlugin.cs # implementazione di ISchulyPlugin
├── Schuly.Plugin.Example.csproj
└── Schuly.Plugin.Example.slnx # apre questo plugin in modo autonomo in un IDEI plugin reali crescono aggiungendo, quando serve, una cartella Controllers/, Data/ (un DbContext + migrazioni EF) e Services/ - guarda src/Schuly.Plugin.Schulware/ o src/Schuly.Plugin.OdaOrg/ per vedere come si presenta un plugin che dialoga con un database e un'API esterna. Nulla di tutto ciò è necessario per iniziare.
Copia
src/Schuly.Plugin.Example/insrc/Schuly.Plugin.<Name>/:src/Schuly.Plugin.<Name>/ ├── <Name>Plugin.cs ├── Schuly.Plugin.<Name>.csproj └── Schuly.Plugin.<Name>.slnxRinomina
ExamplePlugin.cs→<Name>Plugin.cs; rinomina la classe e il namespace di conseguenza. La classe implementaISchulyPlugin.Rinomina il
.csproj(e lo.slnx) inSchuly.Plugin.<Name>. Mantieni<TargetFramework>net10.0</TargetFramework>e laPackageReferenceaSchuly.Plugin.Abstractions. Imposta<Version>,<Description>,<Authors>- questi dati confluiscono nell'indice di distribuzione pubblicato.Apri una issue con etichetta
new-plugin, poi segui il flusso di contribuzione: branch → PR (Closes #<issue>) → squash-merge.
Non serve alcuna modifica al workflow - build_push.yml individua automaticamente ogni src/Schuly.Plugin.*/*.csproj. Vedi setup/distribution.md.
Il ciclo di vita di ISchulyPlugin
La classe del plugin è la composition root (snella, come un Program.cs). L'host dei plugin del backend, all'avvio, invoca questi metodi in ordine:
ConfigureServices(IServiceCollection services, PluginServiceContext context)
Registra qui i tuoi servizi, le opzioni, il task in background e il login. context espone:
context.ConnectionString- la connection string Postgres dedicata al plugin (l'host la modifica per puntare aschuly_plugin_<name>; vedi migrations.md).context.Configuration- la configurazione YAML del plugin (Schuly.Plugin.<Name>.yml).
Registrazioni tipiche (da Schulware/OdaOrg):
services.AddDbContext<MyDbContext>(o => o.UseNpgsql(context.ConnectionString));
services.AddSingleton<MySyncTask>();
services.AddSingleton<IPluginBackgroundTask>(sp => sp.GetRequiredService<MySyncTask>());
// Vault dei segreti in memoria, isolato per plugin, indicizzato dall'host tramite il nome del plugin.
services.AddScoped(sp => new MySecretStore(
sp.GetRequiredKeyedService<IPluginVault>(MyPlugin.PluginName)));
services.AddScoped<IPluginLogin, MyLogin>();La chiave del vault deve essere una costante (usata con
[FromKeyedServices(PluginName)]), quindi esponi unpublic const string PluginName. Il vault esiste solo in memoria - i segreti non sopravvivono a un riavvio del backend, e il codice di sincronizzazione deve gestire il caso di vault vuoto ("serve una nuova connessione").
ConfigureEndpoints(IEndpointRouteBuilder endpoints)
Qui si mappano le route in API minimale. Il plugin Example lo usa direttamente:
endpoints.MapGet("/api/plugins/example/hello",
(IPluginUserContext userContext) => Results.Ok(...)).RequireAuthorization();
endpoints.MapGet("/api/plugins/example/info", () => Results.Ok(...)).AllowAnonymous();I plugin Schulware e OdaOrg lasciano invece questo metodo vuoto e mettono le route in Controllers/ come controller ASP.NET MVC - l'host registra l'assembly del plugin come ApplicationPart MVC, quindi i controller [ApiController] vengono scoperti automaticamente. Entrambi gli approcci funzionano; i controller si adattano meglio a superfici più ampie.
MigrateAsync(IServiceProvider serviceProvider, CancellationToken)
Applica le migrazioni EF Core. Risolvi il DbContext da uno scope e chiama db.Database.MigrateAsync(). Il plugin Example (senza database) restituisce semplicemente Task.CompletedTask. Vedi migrations.md - usa MigrateAsync(), mai EnsureCreatedAsync.
IPluginBackgroundTask (opzionale)
Lavoro ricorrente. Implementa Name, Interval ed ExecuteAsync. Il PluginBackgroundTaskHost del backend invoca ExecuteAsync a ogni tick di Interval:
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();
// risolvere i servizi a scope ridotto, eseguire il lavoro
}
}