Skip to content

Clone, docker compose up, Go: Friction-Free EF Core Integration Testing with Weasel and Wolverine

Jeremy Miller30th August 2026
WeaselWolverineEF CoreTestingCritter Stack
Weasel

Here's a small ritual that I think quietly costs .NET teams more than anyone measures. A new developer joins, clones the repository, and then spends the rest of the morning getting the database into a state where the test suite will run. Start a container. Find the connection string. Run dotnet ef database update — against the right DbContext, in the right order, after remembering which project holds the migrations. Discover that a migration authored on someone else's branch has since been squashed. Ask in Slack. Eventually, tests go green.

Then the second ritual: the test suite itself. Tests that pass alone and fail together, because test #14 left rows behind that test #37 didn't expect. So somebody writes a cleanup helper, somebody else writes a different cleanup helper, and now half the suite runs inside a rolled-back transaction and the other half doesn't, and nobody wants to touch either one.

Marten users have been spared both of these for about a decade, and they mostly don't realize it. That experience comes from Weasel, the Critter Stack's schema engine. What's new is that Weasel now does exactly the same two things for your EF Core DbContext types — and Wolverine wires it up for you.

The Marten experience, stated plainly

The thing Marten users take for granted is this: you never write a migration. You change your configuration, you restart the application, and the database catches up. There's no ordered chain of Up()/Down() classes checked into source control, no history table to reconcile, no merge conflict when two branches both add a migration. Weasel reads your model, introspects the live database, computes the delta, and applies it.

That's the "it just works" schema management that has been Marten's calling card since the beginning, and it's the single feature I hear about most often from people who've been running Marten in production for years. It's also, not coincidentally, why Marten's own test suite can run against a bare PostgreSQL container with no setup step at all.

Weasel is now a standalone, provider-neutral engine — I wrote about that architecture separately in Meet Weasel. And because it's standalone, we could point it at a source of schema truth that isn't Marten: an EF Core DbContext.

Weasel-managed migrations for EF Core

The Weasel.EntityFrameworkCore package maps EF Core's model metadata onto Weasel's schema objects, which means every DbContext in your application becomes just another thing Weasel knows how to make true in a database. In a Wolverine application it's one line:

csharp
builder.UseWolverine(opts =>
{
    opts.PersistMessagesWithSqlServer(connectionString);

    opts.Services.AddDbContextWithWolverineIntegration<ItemsDbContext>(
        x => x.UseSqlServer(connectionString));

    // Diff every registered DbContext against the live database
    // at startup and apply whatever DDL is missing
    opts.UseEntityFrameworkCoreWolverineManagedMigrations();
});

With that call in place, Wolverine applies the schema for all known DbContext types alongside its own envelope storage tables and any Marten or Polecat schema in the same application. One startup, one pass, one source of truth. The Wolverine EF Core migrations guide covers the wiring; the Weasel EF Core docs cover the diff engine underneath it.

So the onboarding story becomes:

bash
git clone ...
docker compose up -d
dotnet test

No migration step. Not because it was hidden somewhere clever, but because there isn't one to run — the application makes the database match itself. Point that at a fresh PostgreSQL container, a fresh SQL Server container, MySQL, SQLite, or even Oracle, and it behaves the same way. Weasel ships a Migrator for all five, and the EF Core mapping goes through that same abstraction rather than being written once per database.

But does it produce the right schema?

This is the fair question, and it's the one we spent the most effort on. "Weasel creates a schema for your DbContext" is only useful if that schema is the one EF Core itself would have created — otherwise EF's own runtime SQL won't match the tables you're handed.

So the mapping is verified by a dual-schema comparison harness. For each permutation DbContext: EF Core creates the schema through its own GenerateCreateScript(), a neutral catalog introspector (querying pg_catalog and sys.* directly, trusting neither EF nor Weasel) snapshots the result, and then Weasel's delta detection runs against that EF-created schema and must report no difference. Weasel is never allowed to want to "migrate" a schema EF Core just built. Then the schema is dropped, Weasel builds it from scratch, and the two catalog snapshots are diffed field by field — columns, types, nullability, defaults, identity, primary keys, foreign keys with their delete actions, indexes with filters and INCLUDE columns and sort order, and check constraints.

That harness found real bugs, and fixing them is most of what shipped in the EF Core sweep: PostgreSQL identifier casing (EF emits quoted "BlogId", Weasel used to fold to lowercase, and EF's SQL then couldn't find its own columns), indexes that weren't mapped at all — which meant a CreateOrUpdate migration would have dropped EF's conventional IX_* foreign key indexes — literal HasDefaultValue(...) defaults being silently discarded, client-side delete behaviors mapped to real ON DELETE clauses, and integer keys created without IDENTITY / GENERATED BY DEFAULT AS IDENTITY so inserts through EF simply failed. Catalog-level parity is verified for PostgreSQL and SQL Server. The full accounting is in the Weasel Grows Up on EF Core post and the table mapping reference.

Weasel's more recent releases have been mostly this same kind of work, and it matters more than it sounds. The 9.25 through 9.27 releases were largely about read-back fidelity — cases where Weasel introspected a schema as something other than what the database actually held, which produces a delta that can never converge: the patch applies, the read-back still differs, and the next run generates the identical patch forever. A named foreign key on SQLite that rebuilt the whole table on every startup. A partition-aligned index on SQL Server that got dropped and recreated on every run. An invalid PostgreSQL index that wasn't recognized as drift. Those are exactly the bugs that turn "just restart the app" from a feature into a liability, which is why they got the attention. The details are in the 9.26 and 9.27 upgrade notes.

When you do want EF Core migrations

None of this is an argument that migration files are worthless. Plenty of shops need a reviewable, auditable change script for production deploys, and that's a legitimate requirement rather than a habit. Weasel can now generate standard, compilable EF Core migration files from its schema model too — the exact opposite direction, shipping v1 for PostgreSQL and SQL Server — which I wrote up in Generate EF Core Migrations From Your Real Database Schema with Weasel.

The combination is the point: let Weasel apply the schema directly in local development and integration tests where iteration speed is everything, and generate migration artifacts for the production pipeline where the audit trail is everything. Just don't let both streams own the same table — the coexistence guide is blunt about that rule.

The second half: resetting state between tests

Getting the schema up is half the friction. The other half is what happens between tests.

The canonical .NET answer here is Respawn, Jimmy Bogard's small, sharp library that intelligently deletes all data from a database in foreign-key-safe order. It's excellent, it's been the right answer for years, and it's the direct inspiration for what we built. Marten has had its own version of this idea for a long time in IDocumentStore.Advanced.ResetAllData(), paired with Marten's IInitialData for reestablishing a known baseline afterward — the pattern that shows up throughout Marten's integration testing guide.

Weasel now brings that same pairing to EF Core. The database cleaner discovers your tables from DbContext metadata — not from a live catalog scan — resolves the foreign key ordering, and generates provider-specific SQL to truncate everything safely:

ProviderStrategyIdentity reset
PostgreSQLTRUNCATE ... RESTART IDENTITY CASCADEAutomatic
SQL ServerDELETE FROM in FK orderDBCC CHECKIDENT
SQLiteDELETE FROM in FK orderClears sqlite_sequence
MySQLTRUNCATE TABLE with FOREIGN_KEY_CHECKS=0Automatic
OracleDELETE FROM in FK orderSequence-based

Because the EF Core model is immutable, the cleaner memoizes the dependency graph and the generated SQL on first use. Every subsequent reset in your suite reuses the cached statement — which is the difference between a reset you can afford to run before every test and one you can't.

IInitialData, the Weasel version

An empty database isn't usually what a test wants. It wants a known database. That's IInitialData<TContext>:

csharp
public class SeedCoreItems : IInitialData<ItemsDbContext>
{
    public async Task Populate(ItemsDbContext context, CancellationToken cancellation)
    {
        context.Items.AddRange(
            new Item { Name = "Alpha" },
            new Item { Name = "Beta" });

        await context.SaveChangesAsync(cancellation);
    }
}

builder.Services.AddInitialData<ItemsDbContext, SeedCoreItems>();

There's a lambda overload for the cases where a whole class is overkill:

csharp
builder.Services.AddInitialData<ItemsDbContext>(async (ctx, ct) =>
{
    ctx.Items.Add(new Item { Name = "Gamma" });
    await ctx.SaveChangesAsync(ct);
});

Every registered seeder runs, in registration order, on every reset. That's what makes layering work — a baseline seeder for the whole suite, plus a conditional one that only registers in Development, plus a feature-specific one. Much easier to live with than the single giant seeder class that every team eventually grows.

And because the cleaner always deletes first, seeders can use fixed primary keys without collision worries. That's a quietly large win for assertions: look up a known Guid instead of threading a reference back out of setup code.

Putting it together in a test

In a Wolverine application, the cleaner is registered for you by UseEntityFrameworkCoreTransactions() — there's no AddDatabaseCleaner<T>() to remember. A test is then just:

csharp
[Fact]
public async Task ordering_flow()
{
    // Wipe this DbContext's tables in FK-safe order,
    // then re-run its IInitialData<T> seeders
    await _host.ResetAllDataAsync<ItemsDbContext>();

    // arrange ... act ... assert
}

host.ResetAllDataAsync<T>() is scoped to one DbContext, and that's deliberately the recommended default. There's a bigger hammer — host.ResetResourceState() resets every IStatefulResource in the host: Wolverine's message store, every broker, every DbContext cleaner — and it's the right call when a test genuinely spans several stores. But resetting the entire world before every test multiplies your suite runtime for no benefit. Use the finest-grained mechanism the test actually needs. More on both in the Wolverine EF Core development-time guide and the initial data page.

One caveat worth stating outright, because it's the mistake I'd expect: IInitialData runs on reset, not on every application start. It is not a production bootstrap mechanism. For data that must exist on first deploy, use EF Core's own seeding or an explicit setup command.

What this actually buys you

Add it up and the developer loop looks like this. Clone the repository. Start whichever container your database of choice needs. Run the tests — the application builds the schema for every DbContext it knows about, plus Wolverine's envelope tables, plus any Marten or Polecat schema, in one startup pass. Each test resets its own slice of the database to a known baseline in a memoized, FK-safe statement. Change a model, restart, keep going; there is no migration to author and none to run.

That's not a new idea. It's the Marten experience, which was itself standing on the shoulders of tools like Respawn — now available to the EF Core half of your application, and to teams who aren't using Marten at all.

If you'd like help getting an existing EF Core codebase onto this loop — especially the awkward middle ground where you have a real migration history you can't just abandon — that's very much what a JasperFx support plan is for, and we're always around in Discord to talk it through.

RSS Feed · All Rights Reserved.