Skip to content

Clone n' Go with Stateful Resources and Weasel

Jeremy Miller10th November 2026
Weasel

We've spent a lot of time and energy trying to give the entire Critter Stack a good getting started story that should not require the most expensive AI model. A key part of that is our stateful resource model and our Weasel-backed "it should just work" database migration library. That and a series of sensible defaults (you can argue about whether you agree with our defaults of course).

I've spent a lot of my career walking into existing enterprise systems, and on day one being pointed at a long Wiki page that hopefully tells you everything you need to know about how to get the codebase running on your box. That's almost always a miserable experience as plenty of the instructions might easily be outdated. On the flip side, one of my OSS projects (FubuMVC) failed in no small part because it required too much work to get started on a new project.

I do think that life as an enterprise developer got much easier with the advent of Docker for development time infrastructure dependencies. .NET itself is a far better ecosystem after the .NET Core wave of changes created common idioms across the ecosystem for configuration, application bootstrapping, and how libraries like Marten or Wolverine can be integrated into .NET applications.

In an ideal world, a developer should be able to start from a fresh clone of the codebase and be able to run the system or its integration tests almost immediately. That "clone 'n go" experience has been a design goal across the whole Critter Stack for years, and we've invested a lot of time building the machinery in this post: how Marten, Polecat, Fisher and Wolverine know what their databases should look like, how they make the database match, and how the same mechanism scales from a laptop all the way to a production database where the application isn't allowed to run DDL at all.

I wrote about the broker half of this story recently in The F5 Experience, Even with Rabbit MQ, AWS SQS, or Azure Service Bus. This time though, we're going to focus on the database half, which is older, deeper, and in some ways the thing that started it all.

Start with the problem relational databases gave us ​

I still believe that document databases are much more productive for developers compared to any kind of ORM + RDBMS approach when a document database is appropriate for your problem domain.

Back in 2016, when Marten was brand new, I wrote that exhibit #1 in the case against relational databases is the need for laboriously tracking database migrations. Compare the steps for adding a new property to a persisted object. With a relational database and any ORM, "micro" or otherwise, you:

  1. Add the new property
  2. Write a migration script that adds the column
  3. Change your ORM mapping or your SQL to reflect the new property

With a document database approach like Marten's, you:

  1. Add the new property and continue on with your day

That's still true today, and still an argument in favor of document storage. Marten, Polecat, Fisher, Wolverine, and CritterWatch do still need real tables, indexes, functions and sequences of course, and they've got to be built out or migrated when the configuration changes. I originally thought that the automatic "it should just work" style of migrations would mostly be used at development or testing time, but plenty of users happily use that out of the box for their production deployments. Other shops of course want more control or have existing database migration tooling or processes. Our Weasel library tries to be friendly to a wide variety of database change management approaches by allowing you to export SQL scripts including patch and drop down files that can be incorporated in many common database migration tools -- and that even includes EF Core migrations.

Weasel has grown quite a bit, to many more database schema features and more database engines as well. It started as PostgreSQL only, then added SQL Server, and finally Oracle, MySQL, SQLite, and Firebird this year.

Marten knows what its database should look like ​

Here's the smallest possible Marten program. Point it at an empty PostgreSQL database where you have full rights, and it just works:

csharp
public class Customer
{
    // A property called "Id" is assumed to be the primary key
    // for a document type in Marten. This is one of our
    // "sensible defaults"
    public Guid Id { get; set; }
    public string? Region { get; set; }
    public ContractDuration Duration { get; set; }
}

public record ContractDuration(DateOnly Start, DateOnly End);

await using var store = DocumentStore
    .For("Host=localhost;Port=5432;Database=marten_testing;Username=postgres;password=postgres");

var customer = new Customer
{
    Region = "West Coast",
    Duration = new ContractDuration(new DateOnly(2026, 1, 1), new DateOnly(2026, 12, 31))
};

await using var session = store.LightweightSession();
session.Store(customer);
await session.SaveChangesAsync();

var customer2 = await session.LoadAsync<Customer>(customer.Id);

Nothing in that code mentions a table. The first time a session stores a Customer, Marten checks whether the mt_doc_customer table, its upsert function and its indexes exist and match the configured document type, and if they don't, it creates or alters them. The event store tables, the Hi-Lo sequence table and everything else work the same way: checked and built the first time they're needed, then never looked at again for the life of the process.

What Marten is allowed to do when it finds a difference is the AutoCreate policy, and it's the first knob to understand:

csharp
var store = DocumentStore.For(opts =>
{
    // Create what's missing, alter what it can, and drop and
    // recreate a table it cannot patch in place. The only mode
    // that can lose a whole table's data.
    opts.AutoCreateSchemaObjects = AutoCreate.All;

    // Create what's missing and alter existing tables to match the
    // configuration. Never drops a whole table -- but the update
    // DOES drop columns, indexes and foreign keys that the
    // configuration no longer declares. This is the default.
    opts.AutoCreateSchemaObjects = AutoCreate.CreateOrUpdate;

    // Create what's missing, never touch anything that exists
    opts.AutoCreateSchemaObjects = AutoCreate.CreateOnly;

    // Never change the schema at runtime
    opts.AutoCreateSchemaObjects = AutoCreate.None;
});

Two things about these modes bite people, so let me say them plainly:

  • CreateOrUpdate is not "additive only." It never drops a table, but for a table it manages it brings the database in line with the configuration in both directions. Remove a duplicated field from a document and the next migration drops that column, data and all. If a deployment must never lose a schema object, CreateOnly refuses the migration instead of applying it.
  • None does not validate anything. The lazy check simply returns, so a missing or stale table shows up later as PostgreSQL's own 42P01: relation does not exist, not as a Marten message about drift. Validation is a separate, explicit step we'll get to below.

Polecat on SQL Server and Fisher on SQLite have exactly the same AutoCreateSchemaObjects property with exactly the same four values, and Wolverine's message storage has it as AutoBuildMessageStorageOnStartup. That's not a coincidence.

The same engine under every critter ​

We have done a lot of work in 2026 to generalize the underpinnings of Weasel to eliminate duplicated code across the database engines we support and try to make Weasel itself easier for us to maintain.

The reason every tool has the same knob is that every tool has the same migration engine underneath it. Weasel was extracted from Marten during the V4 rewrite, and it's now the schema library for the whole stack. Here's what a table looks like to Weasel:

csharp
var table = new Table("myapp.people");
table.AddColumn<int>("id").AsPrimaryKey();
table.AddColumn<string>("first_name").NotNull();
table.AddColumn<string>("last_name").NotNull();
table.AddColumn<string>("email");

table.ModifyColumn("email").AddIndex(i => i.IsUnique = true);

await using var conn = await dataSource.OpenConnectionAsync();

// Creates the table if it's missing, or applies only the
// difference if it already exists
await table.MigrateAsync(conn);

Behind MigrateAsync(), Weasel reaches into the database catalog to find the current state of that table, if any. If the table doesn't exist, Weasel creates it from the in-memory definition. If it does exist, Weasel computes the delta between the expected table and the actual one and issues only the SQL that closes the gap: add a missing column, remove a column that isn't in the definition, add or rebuild an index, fix up a foreign key. If there's no difference, Weasel does absolutely nothing. The same model covers functions, stored procedures, sequences, PostgreSQL extensions, table partitioning, and SQL Server's user-defined table types, across PostgreSQL, SQL Server, SQLite, MySQL, Oracle and Firebird.

Every table in the Critter Stack is one of those Table objects:

  • Marten's document tables, event store tables and Hi-Lo table
  • Polecat's pc_* tables and Fisher's fi_* tables
  • Wolverine's inbox, outbox, dead letter, node and agent tables, and the queue tables of its database-backed transports
  • The tables of an EF Core DbContext that Wolverine has been asked to manage
  • The CritterWatch console's own storage

Which means the rest of this post applies to all of them at once. When you learn how Marten's schema gets into production, you've learned how Wolverine's does, and Polecat's, and Fisher's. There is one engine, one AutoCreate policy, and, as we're about to see, one set of commands.

The stateful resource model ​

Here's where it gets more interesting than "a library that creates tables." A database schema is only one kind of thing an application needs to exist before it can run. Wolverine with Rabbit MQ needs exchanges, queues and bindings. With Azure Service Bus it needs topics and subscriptions. With SQS it needs queues. All of those are stateful resources: external, persistent, and required to be in a known shape before the first message moves.

The shared JasperFx library describes all of them with one small interface:

csharp
public interface IStatefulResource
{
    string Type { get; }
    string Name { get; }
    Uri SubjectUri { get; }
    Uri ResourceUri { get; }

    // Check whether the configuration for this resource is valid.
    // Throw if it isn't
    Task Check(CancellationToken token);

    // Clear any persisted state within this resource
    Task ClearState(CancellationToken token);

    // Tear down the stateful resource represented by this implementation
    Task Teardown(CancellationToken token);

    // Make any necessary configuration to this stateful resource
    // to make the system function correctly
    Task Setup(CancellationToken token);

    // Optionally return a report of the current state of this resource
    Task<IRenderable> DetermineStatus(CancellationToken token);
}

Marten exposes its database as a stateful resource, where Setup() is the Weasel migration and Check() is the assertion that the database matches the configuration. Polecat and Fisher do the same. Wolverine exposes its envelope storage as one, and every one of its broker transports exposes one too. CritterWatch's console exposes its own store. Once everything is behind that interface, two very useful things fall out.

One line for development ​

From Wolverine's kitchen sink sample, which is Marten plus Rabbit MQ in one web application:

csharp
var builder = WebApplication.CreateBuilder(args);

builder.Host.ApplyJasperFxExtensions();

builder.Host.UseWolverine(opts =>
{
    opts.PublishAllMessages()
        .ToRabbitExchange("issue_events", exchange => exchange.BindQueue("issue_events"))
        .UseDurableOutbox();

    opts.ListenToRabbitQueue("issue_events").UseDurableInbox();

    opts.UseRabbitMq(factory =>
    {
        factory.HostName = "localhost";
        factory.Port = 5672;
    });
});

// Registers an IHostedService that runs Setup() on every known
// stateful resource at startup: the Marten tables, the Wolverine
// envelope tables, and the Rabbit MQ objects
builder.Services.AddResourceSetupOnStartup();

builder.Services.AddMarten(opts =>
{
    opts.Connection(builder.Configuration.GetConnectionString("postgres")!);
    opts.DatabaseSchemaName = "issues";
    opts.RegisterDocumentType<Issue>();
}).IntegrateWithWolverine(x => x.MessageStorageSchemaName = "issue_service");

var app = builder.Build();

app.MapGet("/", () => "Hello World!");

// Actually important to return the exit code here!
return await app.RunJasperFxCommands(args);

With AddResourceSetupOnStartup() in place, the procedure for a new developer is: clone the repository, docker compose up -d, and run the tests or hit F5. The hosted service is registered first, so every resource is set up before any other hosted service starts, and it respects the AutoCreate policy, so the same line can stay in the code in every environment. For test harnesses there's AddResourceSetupOnStartup(StartupAction.ResetState), which runs Setup() and then ClearState() so every test host starts from a clean, known state, and IHost.ResetResourceState() does the same from inside a fixture.

And a command line for everything else ​

Because that Program.cs ends with RunJasperFxCommands(args), the same application can be asked to manage its own infrastructure from the shell:

bash
dotnet run -- resources list         # what stateful resources does this app know about?
dotnet run -- resources check        # are they all in place and reachable? Non-zero exit if not
dotnet run -- resources setup        # create or migrate all of them
dotnet run -- resources statistics   # queue depths, table counts, and so on
dotnet run -- resources clear        # purge queues and clear message storage, keep the structure
dotnet run -- resources teardown     # delete all of it

Every action takes --type and --name filters, so you can set up only the Wolverine resources or clear only one queue. And because every stateful resource contributes an environment check, dotnet run -- check-env tells a developer that Docker isn't running in one friendly line instead of a stack trace from somewhere deep inside Npgsql.

Alongside resources, any Critter Stack application with a database has Weasel's own commands, which act on the Weasel databases alone and have the database-specific options:

bash
dotnet run -- db-list                      # the databases this host manages, and the id to pass to -d
dotnet run -- db-apply                     # apply every outstanding migration
dotnet run -- db-assert                    # non-zero exit if any database differs from the configuration
dotnet run -- db-patch migrations/42.sql   # the delta against the live database, plus a rollback file
dotnet run -- db-dump schema.sql           # the whole schema as DDL

If you've used Marten for a while you'll recognize these as the old marten-apply and marten-patch commands. They moved down into Weasel so that Wolverine, Polecat, Fisher and CritterWatch could have them too, and so that a Marten plus Wolverine application has one db-apply that migrates both the document store and the envelope storage. In a multi-tenant application with a database per tenant, every one of those commands walks every tenant database, and db-apply --parallel 8 will do eight physical databases at a time, which matters when "every tenant database" means a thousand of them.

Four ways to get the schema into place ​

So now we have a migration engine, a policy for what it may do, and two ways to invoke it. The thing you actually choose per environment is when the migration runs and who runs it. There are four answers, and they compose.

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 plenty of production systems that let the application own its schema
3. As a deployment stepdotnet run -- resources setup or db-applyOnce per deployment, before the new version startsProduction, when the deployment identity may run DDL
4. From reviewed SQL scriptsA DBA or a migration tool, from db-patch / db-dump outputWhenever change control says soProduction, when the application's credentials cannot run DDL at all

The first two are what we've covered so far, and they're the right answer on a laptop and in a test suite. Let me spend the rest of the post on production.

Turn it off in one place ​

Whichever production shape you pick, step one is usually to stop the application migrating on its own. For a long time that meant setting Marten's AutoCreateSchemaObjects and Wolverine's storage setting separately, with slightly different syntax, and I personally watched someone on Discord get burned by configuring one and not the other. Both now take their default from the active JasperFx profile, so this is the whole of it:

csharp
builder.Services.CritterStackDefaults(x =>
{
    // In "Production", neither Marten nor Wolverine will migrate
    // anything on their own. Development keeps the default of
    // CreateOrUpdate, so the F5 experience is untouched
    x.Production.ResourceAutoCreate = AutoCreate.None;
});

The profile is picked by IHostEnvironment, so there are no if (builder.Environment.IsProduction()) blocks cluttering up the bootstrapping. The same place holds ResourceMigrationFailureMode, which decides whether a startup migration failure aborts the process or logs and continues. The latter is what a replica that lost the race for the migration lock during a rolling deploy wants, because the winning replica has already made the schema current.

Option 3: the application migrates itself, on purpose ​

With the policy at None, run the migration as its own step in the pipeline, using the application's own binary:

bash
dotnet run -- resources setup      # databases AND broker objects
dotnet run -- resources check      # fail the deploy here if anything didn't land

This is the production recipe I recommend when the deployment identity is allowed to run DDL. An explicit resources setup or db-apply is treated as intent to provision, so it always applies the migration as CreateOrUpdate even though the configured policy is None, and CreateOrUpdate never drops a table. There's no migration history table to maintain and no scripts to write, because the configured schema is the migration: Weasel compares the configuration to the live database and applies the difference, the same way it would have at runtime, just at a moment you chose.

If you're hosting with .NET Aspire, the JasperFx.Aspire package exposes the same command as a button on the dashboard and as a startup gate:

csharp
builder.AddProject<Projects.Api>("api")
    .WithReference(db)
    .WaitFor(db)
    .WithJasperFxStartup(c =>
    {
        c.Run("resources", "setup");  // provision before the service takes traffic
    });

Option 4: somebody else runs the SQL ​

It's somewhat unlikely that any self-respecting DBA is going to allow your application to execute schema changes against production, and I wrote that sentence in 2016 too. For that world, generate the SQL and hand it over:

bash
dotnet run -- db-dump schema.sql           # everything, for a brand new environment
dotnet run -- db-patch migrations/42.sql   # only the delta, with a 42.drop.sql rollback beside it

db-patch connects to the target database, compares it to the configuration, and writes the forward migration and the rollback. It changes nothing. The files can go through a pull request, live in source control next to the code that needs them, and be applied by a DBA or by a migration tool like grate, which is exactly the pipeline the Marten DevOps guide walks through. The application runs with AutoCreate.None and never attempts DDL.

And for the teams that have standardized on EF Core migrations as their deployment artifact, Weasel can go one step further and generate real EF Core migration files from its schema model, Marten and Wolverine tables included, so dotnet ef database update applies them with the history table your DBAs already know.

Whatever you pick, assert it ​

Here's the part I'd underline. AutoCreate.None means "don't migrate," not "tell me if I'm out of date." If the scripts weren't applied, or were applied from last week's build, the application will start happily and fail on the first query that touches the missing table. So in every one of these shapes, the gate is:

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 including the brokers

Both work regardless of the AutoCreate setting and neither one changes anything. AutoCreate.None plus db-assert in the pipeline is the combination that actually fails fast on drift. It's also a fine environment test to run against a restored copy of production before release day, with the new build, to see exactly what the release will want to change.

A worked example: a console under a DBA-owned role ​

CritterWatch is a good test of all this, because it's a Critter Stack application we ship to other people's databases, and plenty of those databases are run by a DBA team that is not going to hand a monitoring console db_owner. The console's own tables are Weasel objects and its store is a stateful resource, so the same four options apply, and the Database Schema and Permissions page is the honest version of what each one means for a product that partitions a metrics table by month and adds tables on upgrade.

The recommended deployment there is a middle path worth knowing about: the console keeps migrating its own schema at runtime, but inside two schemas owned by its own role. On PostgreSQL that's the whole grant:

sql
CREATE ROLE critterwatch_app LOGIN PASSWORD '...';

CREATE SCHEMA critterwatch AUTHORIZATION critterwatch_app;
CREATE SCHEMA critterwatch_wolverine AUTHORIZATION critterwatch_app;

No CREATE on the database, no rights in public, nothing elevated. The DBA team gets db-dump and db-patch output to review before an upgrade, db-assert to prove the database matches afterwards, and the application never has the ability to touch anything outside its two schemas. That's the stateful resource model doing exactly what it was built for: the application knows precisely what it needs, says so in a form a human can review, and can be confined to it.

Summing up ​

The whole thing comes down to a short list:

  • Every Critter Stack tool describes its schema in code, as Weasel schema objects, so it can always compute the difference between what it needs and what's there. Marten, Polecat, Fisher, Wolverine and CritterWatch share the engine, the AutoCreate policy and the commands.
  • AutoCreate says what the application may do on its own: CreateOrUpdate by default, None in a production that migrates some other way. Set it once with CritterStackDefaults.
  • Everything stateful is behind IStatefulResource, databases and brokers alike, so AddResourceSetupOnStartup() gives you the F5 experience and resources setup gives you a deployment step, from the same code.
  • The db-* commands are Weasel's and do the database-specific work: db-patch and db-dump for a DBA, db-apply for a pipeline, db-list and --parallel for a fleet of tenant databases.
  • db-assert and resources check never write, and they are the gate, in every environment.

For the reference material: Weasel's documentation has the migration engine and the AutoCreate policy and the command line tools, JasperFx has the stateful resource model, and each tool's docs cover its own tables: Marten, Polecat, Fisher, Wolverine and CritterWatch. Weasel's new Choosing a Schema Management Approach page is the canonical version of the four-approach table above.

And if your team's onboarding still starts with a Wiki page, or your releases still start with a DBA retyping a migration from a ticket, we'd be happy to talk.