Skip to content

Spindle EF Core persistence

BL19 edited this page Aug 25, 2026 · 1 revision

This page describes EF Core setup for a new application and the three supported ways to apply Spindle's migrations:

  1. Database.MigrateAsync()
  2. dotnet ef database update
  3. Generated migration scripts

Install a provider

Install Spindle.Hosting if the application also hosts flows, plus the Spindle EF Core provider package for the database:

Database Package Registration Schema parameter
SQLite Spindle.Persistence.EFCore.Sqlite AddSpindleSqlite(...) No
PostgreSQL Spindle.Persistence.EFCore.PostgreSQL AddSpindlePostgreSql(...) Yes
MySQL Spindle.Persistence.EFCore.MySql AddSpindleMySql(...) No
SQL Server Spindle.Persistence.EFCore.SqlServer AddSpindleSqlServer(...) Yes

For EF Core command-line operations, install the dotnet-ef tool and reference the matching EF Core design package in the application project:

dotnet new tool-manifest
dotnet tool install dotnet-ef
dotnet add package Microsoft.EntityFrameworkCore.Design

Use a design package version matching the EF Core provider major version. The provider packages contain Spindle's provider-specific migrations; the application normally consumes those migrations rather than creating a second copy of them.

Register the provider

Register the provider before AddSpindleRuntime or AddSpindleWorker. The provider registration supplies the Spindle store, pooled IDbContextFactory<SpindleDbContext>, provider migrations assembly, and retrying execution strategy.

SQLite

using Microsoft.Extensions.DependencyInjection;
using Spindle.Persistence.EFCore.Sqlite;

builder.Services.AddSpindleSqlite(
    builder.Configuration.GetConnectionString("Spindle")
        ?? "Data Source=spindle.db");

SQLite also supports the existing SqliteConnection overload. SQLite does not expose a schema parameter because the installed EF Core provider does not support EF Core schemas.

PostgreSQL

using Microsoft.Extensions.DependencyInjection;
using Spindle.Persistence.EFCore.PostgreSQL;

builder.Services.AddSpindlePostgreSql(
    builder.Configuration.GetConnectionString("Spindle")
        ?? throw new InvalidOperationException("The Spindle connection string is required."),
    schema: builder.Configuration["Spindle:Schema"]);

When a schema is supplied, Spindle applies it to entity tables and the migrations history table. Generated migration SQL creates the schema when needed.

MySQL

using Microsoft.Extensions.DependencyInjection;
using Spindle.Persistence.EFCore.MySql;

builder.Services.AddSpindleMySql(
    builder.Configuration.GetConnectionString("Spindle")
        ?? throw new InvalidOperationException("The Spindle connection string is required."));

MySQL does not expose a schema parameter because the installed EF Core provider does not support EF Core schemas.

SQL Server

using Microsoft.Extensions.DependencyInjection;
using Spindle.Persistence.EFCore.SqlServer;

builder.Services.AddSpindleSqlServer(
    builder.Configuration.GetConnectionString("Spindle")
        ?? throw new InvalidOperationException("The Spindle connection string is required."),
    schema: builder.Configuration["Spindle:Schema"]);

SQL Server sensitive-data logging is disabled by default. Provider options can be changed through the optional configuration action, which runs after Spindle's defaults:

builder.Services.AddSpindleSqlServer(
    connectionString,
    schema: schema,
    configure: options =>
    {
        options.EnableDetailedErrors();
        // EnableSensitiveDataLogging() should only be used in a controlled environment.
    });

Design-time context setup

The EF tools must be able to construct the application's SpindleDbContext without starting the production process. The simplest setup is to keep provider registration in the application's normal host-building path:

using Microsoft.Extensions.DependencyInjection;
using Spindle.Hosting;
using Spindle.Persistence.EFCore.PostgreSQL;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSpindlePostgreSql(
    builder.Configuration.GetConnectionString("Spindle")!,
    schema: builder.Configuration["Spindle:Schema"]);

builder.Services.AddSpindleRuntime();

The same registration should be used for runtime and design time so that the migration assembly, history table, provider options, and schema agree. Verify discovery before applying a migration:

dotnet ef dbcontext info \
  --project MyApplication.csproj \
  --startup-project MyApplication.csproj \
  --context SpindleDbContext

If the application cannot be constructed by the EF tools, add an application-owned IDesignTimeDbContextFactory<SpindleDbContext> and configure it with the same provider, connection string, schema, migrations assembly, and options as the application. Do not use a second model with a different schema or migrations assembly.

Who owns the migrations?

Spindle's provider packages contain the migrations for Spindle's own model. A consuming application should normally:

  • Reference one provider package.
  • Configure the provider and optional runtime schema.
  • Apply the migrations during deployment or startup according to its operational policy.
  • Upgrade the provider package when Spindle publishes new migrations.

Do not run migrations add in the consuming application to recreate Spindle's migrations. The repository's add, check, list, and remove helper commands are for maintaining the provider migrations inside the Spindle repository. They use schema-neutral design-time models.

1. Apply migrations with MigrateAsync

This is convenient for local development, integration tests, and a single controlled application instance. It is usually not appropriate for every production replica to run migrations on startup.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Spindle.Persistence.EFCore;

var app = builder.Build();

await using (var scope = app.Services.CreateAsyncScope())
{
    var contextFactory = scope.ServiceProvider
        .GetRequiredService<IDbContextFactory<SpindleDbContext>>();
    await using var database =
        await contextFactory.CreateDbContextAsync();

    await database.Database.MigrateAsync();
}

await app.RunAsync();

For a worker service, run this after builder.Build() and before RunAsync(). For a non-worker service, the same pattern can be used if that service is the designated migration owner.

The configured schema is used for both Spindle's tables and __EFMigrationsHistory. Changing the schema does not move existing data from another schema.

2. Apply migrations with dotnet ef database update

Use this from a developer machine or a dedicated deployment/migration job after dbcontext info succeeds:

dotnet ef database update \
  --project MyApplication.csproj \
  --startup-project MyApplication.csproj \
  --context SpindleDbContext

If the schema comes from application configuration, provide it through the normal configuration source used by the startup project. For example, with an environment-backed Spindle:Schema setting:

Spindle__Schema=spindle dotnet ef database update \
  --project MyApplication.csproj \
  --startup-project MyApplication.csproj \
  --context SpindleDbContext

This command applies the migrations shipped in the configured provider assembly. Treat it as a controlled deployment operation in production; do not run it concurrently from every application replica.

3. Generate and apply migration scripts

Script generation is usually the preferred production workflow when SQL must be reviewed, approved, audited, or applied by a database deployment system.

Generate a complete script:

dotnet ef migrations script 0 \
  --project MyApplication.csproj \
  --startup-project MyApplication.csproj \
  --context SpindleDbContext \
  --output artifacts/spindle-migrations.sql

Generate an idempotent script when the target database may be at different migration levels:

dotnet ef migrations script 0 \
  --idempotent \
  --project MyApplication.csproj \
  --startup-project MyApplication.csproj \
  --context SpindleDbContext \
  --output artifacts/spindle-migrations-idempotent.sql

To generate only a range, provide both migration IDs or names:

dotnet ef migrations script \
  20260809115519_GenericNodes \
  20260825072319_AddConditionWaits \
  --project MyApplication.csproj \
  --startup-project MyApplication.csproj \
  --context SpindleDbContext \
  --output artifacts/spindle-migrations-range.sql

For PostgreSQL or SQL Server, the application's design-time configuration must contain the same schema that production will use. The schema should come from application configuration, for example:

Spindle__Schema=spindle dotnet ef migrations script 0 \
  --idempotent \
  --project MyApplication.csproj \
  --startup-project MyApplication.csproj \
  --context SpindleDbContext \
  --output artifacts/spindle-migrations.sql

The special SPINDLE_EF_SCHEMA variable is for the Spindle repository's own script helper, not a replacement for a consuming application's design-time configuration:

SPINDLE_EF_SCHEMA=spindle \
  ./scripts/migrations.sh script postgresql 0

That helper is useful when validating the provider migrations from a Spindle checkout. In a new application, use the application's own dotnet ef command and configuration as shown above.

Production recommendations

Workflow Good for Production guidance
Database.MigrateAsync() Local development, tests, single-owner startup Avoid running concurrently from every replica.
dotnet ef database update Controlled release or migration job Run from a deployment identity with database permissions.
Generated SQL script Review, approval, audit, DBA-controlled deployment Prefer an idempotent script when deployment state varies.

Whichever workflow is selected, deploy the schema and migration history together with the application version that requires them. Keep connection strings and credentials outside source control.