Skip to content

Choosing a Schema Management Approach ​

Every Critter Stack tool that touches a relational database -- Marten, Polecat, Fisher, Wolverine's durable messaging and EF Core integration, and the CritterWatch console -- describes the tables, indexes, functions and sequences it needs as Weasel schema objects, and lets Weasel work out the difference between that description and a live database. What differs between a developer's laptop and a locked-down production database is not the migration engine but when it runs and who is allowed to run it. This page lays out the four answers to that question, which one to pick for which environment, and where each one is configured.

The four approaches ​

Who runs the migrationWhenTypical use
1. Lazily, at runtimeThe application, the first time it needs a tableOn first use of each featureLocal development
2. At application startupThe application, in a hosted serviceOnce, before anything else startsDevelopment, integration tests, and many production deployments that let the application own its schema
3. As a deployment stepdotnet run -- db-apply or dotnet run -- resources setupOnce per deployment, before the new version startsProduction, where the application runs with AutoCreate.None
4. From reviewed SQL scriptsA DBA or a migration tool, from the output of db-patch / db-dumpWhenever the change control process says soProduction, where the application's credentials cannot run DDL

They compose. A common recipe is 1 or 2 locally, 3 or 4 in production, and db-assert as a gate in both: it tells you whether the database matches the configuration without changing anything.

1. Lazily, at runtime ​

This is the default experience, and the thing that makes a fresh clone of a Critter Stack codebase runnable against an empty database. Each library checks the objects a feature needs the first time that feature is used -- Marten checks a document type's table on the first Store() or query against it, Wolverine checks its envelope tables when the durability agent starts -- and creates or alters whatever is missing.

What it is allowed to do is governed by the AutoCreate policy:

AutoCreateLazy behaviour
AllCreate, alter, and when an object cannot be altered in place, drop and recreate it. The only mode that can lose a table's data
CreateOrUpdate (default)Create what is missing and alter what exists to match, which includes dropping columns and indexes the configuration no longer declares
CreateOnlyCreate what is missing, never alter what exists
NoneDo nothing. A missing table surfaces as the database's own error

None is not a validation mode

With AutoCreate.None, the lazy check simply returns, so a table that is missing or stale shows up later as the provider's error (42P01 relation does not exist on PostgreSQL, Invalid object name on SQL Server, no such table on SQLite) rather than as a message about configuration drift. Validation is a separate, explicit step: db-assert or AssertDatabaseMatchesConfigurationAsync(). See the warning on the AutoCreate policy.

2. At application startup ​

Rather than paying for a schema check the first time each feature is used, apply every outstanding change once, before the rest of the application starts. There are two ways to ask for it, and they do the same work:

  • Per store. Marten, Polecat and Fisher each expose .ApplyAllDatabaseChangesOnStartup() on the result of AddMarten() / AddPolecat() / AddFisher(). Wolverine's envelope storage is applied at startup by default (WolverineOptions.AutoBuildMessageStorageOnStartup).
  • For everything at once. services.AddResourceSetupOnStartup() from JasperFx runs Setup() on every registered stateful resource: every Weasel database the application knows about and the message broker queues, exchanges, topics and subscriptions a Wolverine transport declares. UseResourceSetupOnStartupInDevelopment() on the host builder does the same only when the environment is Development.

The startup hosted service respects the AutoCreate policy. Under None it does nothing, so a production deployment can leave the registration in place and still be sure the application never issues DDL.

What happens when the startup migration fails is governed by ResourceMigrationFailureMode on the active JasperFx profile: fail the startup (the default) or log and continue, which is what a replica losing the migration lock in a rolling deploy wants.

3. As a deployment step ​

Keep the application itself at AutoCreate.None, and run the migration as its own step in the deployment pipeline, with the application's own binary:

bash
dotnet run -- db-apply            # Weasel databases only
dotnet run -- resources setup     # every stateful resource, broker objects included

Both are an explicit instruction to migrate, so both apply the migration as CreateOrUpdate even when the configured policy is None. CreateOrUpdate never drops a table, so this is safe to run against a database with data in it. Follow it with db-assert or resources check and the pipeline fails before traffic is cut over if anything did not land.

This is what the Critter Stack recommends for production when the deployment identity is allowed to run DDL. It needs no migration history table and no hand-written scripts: the configured schema is the migration. The JasperFx.Aspire package exposes the same resources setup as a startup gate and a dashboard button for Aspire-hosted applications.

See Command Line Tools for enabling the commands, and db-apply for the --parallel flag and the per-database reporting that matter when a tenant-per-database store has hundreds of databases.

4. From reviewed SQL scripts ​

When the application's credentials cannot run DDL at all, generate the SQL and hand it to whoever can:

bash
dotnet run -- db-dump schema.sql          # the whole schema, for a new environment
dotnet run -- db-patch migrations/42.sql  # the delta against the live database, plus a rollback file

db-patch connects to the database, compares it to the configuration, and writes the forward migration and a .drop rollback. It changes nothing. The output can be reviewed in a pull request, stored with the application's source, and applied by a database migration tool such as grate as the Marten DevOps guide shows, or by a DBA directly. Run the application with AutoCreate.None so it never attempts the DDL itself, and use db-assert in the pipeline to prove the scripts were applied.

Teams that have standardised on EF Core migrations have a fifth option: EF Core migration generation emits the same Weasel schema model as versioned EF Core migration files that dotnet ef database update applies.

Where the policy is set ​

LibrarySettingDefault
MartenStoreOptions.AutoCreateSchemaObjectsThe active JasperFx profile's ResourceAutoCreate, itself CreateOrUpdate
PolecatStoreOptions.AutoCreateSchemaObjectsCreateOrUpdate
FisherStoreOptions.AutoCreateSchemaObjectsCreateOrUpdate
Wolverine envelope storage and database transportsWolverineOptions.AutoBuildMessageStorageOnStartupThe active JasperFx profile's ResourceAutoCreate, itself CreateOrUpdate
Wolverine EF Core DbContext typesopts.UseEntityFrameworkCoreWolverineManagedMigrations() opts in; the migration then follows the storage setting aboveOff
Weasel used directlyIDatabase.AutoCreateWhatever your DatabaseBase subclass sets

Marten and Wolverine both read their default from the JasperFx profile, so the one-liner that turns off automatic migration for both in production is:

cs
builder.Services.CritterStackDefaults(x =>
{
    x.Production.ResourceAutoCreate = AutoCreate.None;
});

See Critter Stack Defaults for the profile model.

What each library documents ​

Each library's own documentation covers how these approaches apply to its tables, which is where the store-specific caveats live:

Checking rather than changing ​

Whichever approach applies the schema, two commands only ever read:

bash
dotnet run -- db-assert          # non-zero exit if any database differs from the configuration
dotnet run -- resources check    # the same, across every stateful resource

Both work regardless of the AutoCreate setting, so AutoCreate.None plus db-assert in the pipeline is the combination that actually fails fast on drift. db-assert also reports when every object came back missing, which points at a role that cannot see the catalog rather than at a missing schema -- see Introspection is privilege-filtered.

Released under the MIT License.