Skip to main content

EF Core migrations

Plugins that persist data (Schulware, OdaOrg) own their schema via EF Core migrations.

Each plugin gets its own database

The backend's plugin host hands the plugin a dedicated connection string via PluginServiceContext.ConnectionString — it mutates the host connection string so the database name becomes schuly_plugin_<name>. The plugin wires it into its DbContext:

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

Migrations therefore never touch the main Schuly database; each plugin's schema is isolated.

Adding a migration

dotnet ef migrations add <Name> --project src/Schuly.Plugin.Schulware

(Swap the project path for the plugin you're working on.) This writes the migration into the plugin's Data/Migrations/ folder and updates the model snapshot. Requires the dotnet-ef tool — see setup/development.md.

Design-time factory

dotnet ef constructs the DbContext at design time without the runtime DI pipeline, so each plugin ships an IDesignTimeDbContextFactory<T> next to its DbContext (e.g. Data/SchulwareDbContextFactory.cs). The connection string in the factory is a throwaway placeholder — migrations only need the model, not a live database:

internal sealed class SchulwareDbContextFactory : IDesignTimeDbContextFactory<SchulwareDbContext>
{
public SchulwareDbContext CreateDbContext(string[] args) =>
new(new DbContextOptionsBuilder<SchulwareDbContext>()
.UseNpgsql("Host=localhost;Database=schulware_design;Username=postgres;Password=postgres")
.Options);
}

Apply migrations at runtime, not EnsureCreated

Apply pending migrations from MigrateAsync:

public async Task MigrateAsync(IServiceProvider sp, CancellationToken ct = default)
{
using var scope = sp.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<SchulwareDbContext>();
await db.Database.MigrateAsync(ct);
}

Use MigrateAsync(), never EnsureCreatedAsync. EnsureCreatedAsync creates the database on first run but does nothing on later schema changes, so column/index additions would never land on an existing database. MigrateAsync() creates the DB on first run and applies every schema delta afterwards.