Skip to content

Contract reference ​

Everything in the package lives in the Schuly.Plugin.Abstractions namespace and targets net10.0. The contract is 5 interfaces plus three small records. All signatures below are copied verbatim from source under src/Schuly.Plugin.Abstractions/.

The package also ships the backend's Schuly.Domain.dll and Schuly.Infrastructure.dll alongside the abstractions assembly (see the csproj), so plugins can use the backend's typed entities and DbContext for direct DB access. See development for how those are referenced.

ISchulyPlugin ​

The plugin entry point. The backend instantiates one per plugin and drives it through its lifecycle.

csharp
public interface ISchulyPlugin
{
    string Name { get; }
    string Version { get; }
    void ConfigureServices(IServiceCollection services, PluginServiceContext context);
    void ConfigureEndpoints(IEndpointRouteBuilder endpoints);
    Task MigrateAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken = default);
}
MemberPurposeWhen
NameStable plugin identifier.Read at discovery.
VersionPlugin's own version string.Read at discovery.
ConfigureServices(IServiceCollection, PluginServiceContext)Register services, handlers, and options into the host DI container.At startup, before the app is built.
ConfigureEndpoints(IEndpointRouteBuilder)Map the plugin's HTTP endpoints.At startup, after services are built.
MigrateAsync(IServiceProvider, CancellationToken)Run plugin-owned EF Core migrations (db.Database.MigrateAsync()).At startup, after the service provider is available.

PluginServiceContext ​

The context passed to ConfigureServices.

csharp
public record PluginServiceContext(string ConnectionString, IConfiguration Configuration);
MemberPurpose
ConnectionStringThe plugin's database connection string (the host scopes each plugin to its own database).
ConfigurationThe host IConfiguration, for reading plugin options.

IPluginBackgroundTask ​

Recurring background work. The backend's PluginBackgroundTaskHost invokes ExecuteAsync on the task's declared Schedule.

csharp
public interface IPluginBackgroundTask
{
    string Name { get; }
    PluginSchedule Schedule { get; }
    Task ExecuteAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken);
}
MemberPurpose
NameTask identifier (for logging/diagnostics).
ScheduleThe task's default schedule; the host operator can override the cadence.
ExecuteAsync(IServiceProvider, CancellationToken)One execution of the work. Resolve scoped services from serviceProvider.

PluginSchedule ​

The plugin's default schedule for a background task, as a scheduler-agnostic value - it depends on nothing beyond the BCL. The host maps it onto its own scheduler (TickerQ); the host operator can override the cadence per deployment.

csharp
public sealed record PluginSchedule(string Cron, int Retries = 0, IReadOnlyList<TimeSpan>? RetryIntervals = null, bool RunOnStartup = false)
MemberPurpose
CronStandard 5-field cron expression (minute hour day-of-month month day-of-week). Validated in the constructor and on every with-expression; an invalid expression throws ArgumentException.
RetriesNumber of times the host retries a failed execution. Must be >= 0.
RetryIntervalsDelay before each retry. When there are more retries than intervals, the host reuses the last interval for the remaining attempts. null means the host's own default backoff applies.
RunOnStartupWhether the host should also run the task once immediately at startup.
FactoryProduces
Every(TimeSpan interval)A fixed-interval schedule. Only whole minutes dividing 60 (1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30, 60) or whole hours dividing 24 (1, 2, 3, 4, 6, 8, 12, 24) are accepted - anything else can't be expressed as a true fixed-cadence cron and throws ArgumentOutOfRangeException.
Daily(int hour, int minute = 0)A schedule that fires once a day at hour:minute.
Hourly(int minute = 0)A schedule that fires once an hour at minute.
csharp
public sealed class SyncTimetableTask : IPluginBackgroundTask
{
    public string Name => "schulware.sync-timetable";
    public PluginSchedule Schedule => PluginSchedule.Every(TimeSpan.FromMinutes(30));

    public Task ExecuteAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken) => Task.CompletedTask;
}

For a schedule the factories can't express, construct PluginSchedule directly with a cron string and, optionally, retry behaviour:

csharp
public PluginSchedule Schedule => new("0 6 * * MON-FRI", Retries: 3, RetryIntervals: [TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(5)]);

IPluginEventHandler<TCommand> ​

React to a backend command. TCommand is contravariant (in TCommand).

csharp
public interface IPluginEventHandler<in TCommand>
{
    Task HandleAsync(TCommand command, CancellationToken cancellationToken = default);
}
MemberPurpose
HandleAsync(TCommand, CancellationToken)Handle one dispatched command.

IPluginUserContext ​

Read the current user / school-user from inside a plugin.

csharp
public interface IPluginUserContext
{
    Task<Guid> GetCurrentUserIdAsync(CancellationToken cancellationToken = default);
    Task<Guid?> GetCurrentSchoolUserIdAsync(CancellationToken cancellationToken = default);
}
MemberPurpose
GetCurrentUserIdAsync(CancellationToken)The current application user's id.
GetCurrentSchoolUserIdAsync(CancellationToken)The current school-user id, or null if none is in context.

IPluginLogin ​

A plugin's account-connect contract - and the source of its school-system catalog entry. The plugin exposes a SchoolSystem descriptor; the backend collects it from every loaded plugin and seeds the catalog (seed-if-missing by Key), so the operator no longer supplies catalog config. The backend then exposes a single unified login endpoint, resolves the IPluginLogin whose SystemKey matches the requested system, and calls ConnectAsync with the login-field values the app collected from that descriptor. The plugin reads the current user via IPluginUserContext, authenticates against its provider, persists the account, and returns its id. No provider auth lives in the backend.

csharp
public interface IPluginLogin
{
    SchoolSystemDescriptor SchoolSystem { get; }   // the catalog entry this login serves
    string SystemKey => SchoolSystem.Key;          // default member - defaults to SchoolSystem.Key

    Task<PluginLoginResult> ConnectAsync(
        IReadOnlyDictionary<string, string> fields,
        string? displayName,
        CancellationToken cancellationToken = default);
}
MemberPurpose
SchoolSystemThe catalog descriptor the app renders: Key, DisplayName, LoginMethod, PrivateAuthStrategy ("token"/"scrape"), StatelessBasePath, PluginBasePath, SortOrder, and the LoginFields the app collects.
SystemKeyThe catalog system key this login handles, e.g. "schulnetz". Default member returning SchoolSystem.Key; you only implement SchoolSystem.
ConnectAsync(IReadOnlyDictionary<string,string>, string?, CancellationToken)Connect an account from the collected login fields, keyed by the descriptor's LoginFields keys (e.g. "email", "password", "baseUrl"). displayName is an optional friendly name.

SchoolSystemDescriptor (and its LoginFields of SchoolSystemLoginFieldDescriptor) live in Schuly.Plugin.Abstractions; build them in your IPluginLogin:

csharp
public SchoolSystemDescriptor SchoolSystem => new()
{
    Key = "schulnetz",
    DisplayName = "Schulnetz",
    LoginMethod = "credentials",
    PrivateAuthStrategy = "token",
    StatelessBasePath = "/api/plugins/schulware/stateless",
    PluginBasePath = "/api/plugins/schulware",
    LoginFields =
    [
        new() { Key = "baseUrl",  Label = "Schulnetz URL", Type = "url",      Required = true },
        new() { Key = "email",    Label = "Email",         Type = "text",     Required = true },
        new() { Key = "password", Label = "Password",      Type = "password", Required = true },
    ],
};

PluginLoginResult ​

Outcome of ConnectAsync.

csharp
public record PluginLoginResult(bool Success, Guid? AccountId = null, string? Message = null);
MemberPurpose
SuccessWhether the connect succeeded.
AccountIdThe persisted account id when successful.
MessageOptional human-readable detail (e.g. an error reason).

See versioning for the rules that govern changes to any of these members.