Skip to content

Weasel Grows Up on EF Core

Weasel is the low-level database schema management and migration engine extracted from Marten — the same delta-detection and DDL-generation machinery the entire Critter Stack runs on. Over the last three NuGet releases (culminating in 9.18.0 and 9.18.1) the Weasel.EntityFrameworkCore package went from "maps most of an EF Core model" to a genuinely production-grade bridge between Entity Framework Core and Weasel's schema tooling.

This post walks through what landed, with runnable samples and links to the docs. The headline items:

  1. Byte-for-byte schema parity between the schema Weasel generates for a DbContext and the schema EF Core's own migrations would create — verified at the database-catalog level for PostgreSQL and SQL Server.
  2. EF Core migration file generation — the reverse direction, where Weasel emits standard, compilable EF migration artifacts (dotnet ef database update, idempotent scripts, bundles) from its schema model.
  3. A customization hook (new in 9.18.1) that lets callers such as Wolverine's conjoined multi-tenancy decorate mapped tables and inject extra schema objects into the same migration.

Plus the supporting cast that makes the integration pleasant day to day: an FK-aware database cleaner for tests, a batched-query API, and full JSON column mapping.


Why an EF Core bridge at all?

Weasel's job is schema migration: define database objects programmatically, detect what changed against a live database, and generate the DDL to reconcile them. Marten already uses Weasel internally. The EF Core package lets you point that same machinery at a schema you defined through EF Core's fluent API.

The big payoff is mixed applications — Marten (or Wolverine, or Polecat) for event-sourced/document storage and EF Core for relational entities, living in one database and managed by one migration tool. Both flows read from the same source of truth, IDatabase.AllObjects().

📄 EF Core Integration Overview


1. Schema parity: the mapping now matches EF Core exactly

The guiding principle of the mapping sweep was blunt: the schema Weasel creates for a DbContext should be the schema EF Core's own migration system would create — and we should be able to prove it.

So we built a dual-schema comparison harness. For each permutation DbContext:

  1. EF Core creates the schema via its own GenerateCreateScript(), and a neutral catalog introspector (querying pg_catalog / sys.* directly) snapshots the result.
  2. Weasel's delta detection runs against that EF-created schema — it must report SchemaPatchDifference.None. Weasel must never want to "migrate" a schema EF just made.
  3. Weasel then builds the schema from scratch and the two catalog snapshots are diffed field by field.

That harness surfaced — and we fixed — a pile of real divergences that existed on master:

  • PostgreSQL identifier casing. EF emits quoted PascalCase ("BlogId"); Weasel used to fold everything to lowercase, so EF's own SQL couldn't find its columns. There's now an opt-in ITable.PreserveIdentifierCase seam (set automatically by the mapper) with case-insensitive delta comparison throughout. All-lowercase callers like Marten still emit byte-identical DDL.
  • Indexes are mapped now — including EF's conventional IX_* FK indexes, composite, unique, filtered, and covering/INCLUDE indexes, plus alternate keys as unique indexes. Previously Weasel treated EF's indexes as unknown extras and a CreateOrUpdate migration would have dropped them.
  • Literal defaults (HasDefaultValue(...)) render through EF's own SQL literal generator, so CAST(1 AS bit), N'...', TRUE, enum-to-string conversions, etc. match EF's output exactly.
  • Delete behaviors: EF's client-side behaviors (ClientSetNull, ClientCascade, ClientNoAction) now correctly emit no ON DELETE clause.
  • Identity / value generation is mapped — GENERATED BY DEFAULT AS IDENTITY (PostgreSQL) / IDENTITY(1,1) (SQL Server) — with the right suppressions for TPT/owned linking keys and non-integral types.

Here's the core mapping API. MapToTable() turns an EF IEntityType into a Weasel ITable:

csharp
var migrator = new PostgresqlMigrator(); // or SqlServerMigrator
using var context = dbContext;

foreach (var entityType in DbContextExtensions.GetEntityTypesForMigration(context))
{
    var table = migrator.MapToTable(entityType);
    // table is now a Weasel ITable with full schema definition
}

To pull in model sequences (from UseHiLo, UseSequence, HasSequence) and the tables, in dependency order:

csharp
// Sequences first, then the mapped tables — so column defaults that
// reference them (NEXT VALUE FOR ...) are valid when tables are created
var schemaObjects = DbContextExtensions.GetSchemaObjectsForMigration(context, migrator);

The mapper handles TPH (derived required properties → nullable columns), TPT (each type its own table, keyed PK-as-FK with no identity), table-split and JSON owned entities, topological FK sorting, and check constraints. Full matrix in the docs.

📄 Table Mapping

Escaping the neutral seam

Provider-specific index features that the neutral mapping can't express — Npgsql HasMethod("gin"), descending sort — go through a customization hook that downcasts to the concrete provider Table. (More on that hook in section 3.)


2. EF Core migration generation — the reverse direction

This is the big one. Instead of Weasel applying schema changes itself, it can now emit standard EF Core migration files that your team applies with the tooling it already knows.

Weasel-native (db-patch / db-apply)EF migration generation (db-ef-migration)
Schema applied byWeasel at startup or CLIEF Core toolchain (dotnet ef, bundles, scripts)
Change historynone (delta against live DB)versioned migration files + __EFMigrationsHistory
DBA review artifactpatch SQL filemigration .cs files / idempotent script
Best fordev loops, Marten-style auto-migrationteams standardizing on EF migrations

Getting started

bash
dotnet run -- db-ef-migration add Initial

Or programmatically — note the source is IDatabase, so Marten system tables, Wolverine envelope storage, Polecat event storage, and EF projection tables all flow through one door:

csharp
var result = await EfMigrationGenerator.AddAsync(
    database,
    "Initial",
    new EfMigrationGenerationOptions
    {
        Directory = "WeaselMigrations",
        Namespace = "MyApp.WeaselMigrations"
    });

// first run writes three artifacts:
// result.MigrationFile  -> 20260718120000_Initial.cs (attribute-only migration)
// result.ContextFile    -> <Identifier>SchemaDbContext.cs (stub context)
// result.SnapshotFile   -> weasel-schema-snapshot.json (design-time baseline)

The generated files are ordinary EF migrations. Compile them into a project that references the EF provider package and apply them exactly like any other:

bash
export WEASEL_EF_CONNECTION="Host=localhost;Database=app;..."
dotnet ef database update --context MyStoreSchemaDbContext

# idempotent scripts and bundles work too
dotnet ef migrations script --idempotent --context MyStoreSchemaDbContext -o migrations.sql
dotnet ef migrations bundle --context MyStoreSchemaDbContext

The generated stub DbContext relocates its __EFMigrationsHistory into the critter-stack schema so it never collides with your application's own EF context, suppresses the EF 9+ PendingModelChangesWarning, and ships an IDesignTimeDbContextFactory that reads WEASEL_EF_CONNECTION so the EF CLI works without an application host.

Incremental migrations, diffed in memory

The next db-ef-migration add diffs the current model against a serialized schema snapshot (Weasel's analog of EF's ModelSnapshot) — entirely in memory, no live database, no shadow container — and emits an incremental migration with a real reverse-ordered Down():

csharp
var options = new MigrationOperationTranslationOptions(EfMigrationProvider.PostgreSql)
{
    Migrator = new PostgresqlMigrator()
};

var table = new PgTable("app.orders");
table.AddColumn<int>("id").AsPrimaryKey();
table.AddColumn<string>("name").NotNull();

var baseline = EfSchemaSnapshot.FromSchemaObjects(new ISchemaObject[] { table }, options);
var json = baseline.ToJson();

// ... later: the model changed
table.AddColumn<string>("tenant_id").NotNull();
var target = EfSchemaSnapshot.FromSchemaObjects(new ISchemaObject[] { table }, options);

var incremental = EfSnapshotDiffer.Diff(EfSchemaSnapshot.FromJson(json), target, options);
// incremental.UpOperations   -> AddColumn tenant_id
// incremental.DownOperations -> DropColumn tenant_id

For things the snapshot diff deliberately refuses (partition layout changes, rewritten function bodies), pass --against-database to run Weasel's own delta detection against the live database and wrap the resulting SQL in Sql() operations.

Adopting an existing database

Already have a running Marten/Wolverine app with the schema in place? Baseline it — record the generated migrations as applied without executing anything:

csharp
var recorded = await EfMigrationGenerator.BaselineAsync(
    database,
    new EfMigrationGenerationOptions { Directory = "WeaselMigrations" });

Afterwards dotnet ef database update reports nothing pending, and future incrementals apply on top.

What routes through raw SQL

Everything EF can't model goes through migrationBuilder.Sql(...) carrying Weasel's own DDL, so it works from day one: PostgreSQL table partitioning (RANGE/LIST/HASH and the managed strategies), PL/pgSQL functions, SQL Server stored procedures and table types, and expression indexes. v1 providers are PostgreSQL and SQL Server.

📄 EF Core Migration Generation · 📄 Mixed EF + Critter Stack Apps


3. The customization hook (new in 9.18.1)

Real-world integrations need to reach past the neutral mapping — the motivating case was Wolverine's conjoined multi-tenancy, which needs to attach Weasel-managed tenant partitioning to the tables mapped from ITenanted EF entities and inject the partition control/registry tables into the same migration.

That's exactly what EfSchemaMappingCustomization does. It's a two-part hook: decorate each mapped table (CustomizeTable), and contribute additional schema objects that migrate ahead of the entity tables (AdditionalObjects):

csharp
// A control/registry table that must be migrated *ahead* of the
// entity tables that depend on it
var partitionRegistry = new PgTable("tenants.partition_registry");
partitionRegistry.AddColumn<string>("tenant_id").AsPrimaryKey();
partitionRegistry.AddColumn<string>("partition_suffix").NotNull();

var customization = new EfSchemaMappingCustomization
{
    // Called for every table mapped from an EF entity type, after the
    // standard mapping. Downcast to the concrete provider Table to reach
    // provider-specific features the neutral seam can't express.
    CustomizeTable = (IEntityType entityType, ITable table) =>
    {
        if (typeof(ITenantScoped).IsAssignableFrom(entityType.ClrType)
            && table is PgTable pgTable)
        {
            // Attach Weasel-managed LIST partitioning on tenant_id
            pgTable.PartitionByList("tenant_id")
                .AddPartition("acme", "acme")
                .AddPartition("globex", "globex");
        }
    },

    // Extra schema objects migrated ahead of the entity tables
    AdditionalObjects = new ISchemaObject[] { partitionRegistry }
};

// The customization flows through delta detection and DDL generation
await using var migration =
    await serviceProvider.CreateMigrationAsync(dbContext, customization, ct);

The same entity type / table pair is presented on every mapping pass (both CreateDatabase and each CreateMigrationAsync), so partitioning stays attached consistently across delta detection and DDL generation. Contributed objects are emitted first so anything the tables depend on already exists.

📄 Schema Mapping Customization


The supporting cast

FK-aware database cleaner for tests

Inspired by Respawn and Marten's ResetAllData(), the cleaner discovers tables from DbContext metadata, resolves FK ordering, and generates provider-specific truncation SQL:

csharp
services.AddDbContext<ShopDbContext>(o => o.UseNpgsql("Host=localhost;Database=mydb"));
services.AddSingleton<Migrator, Weasel.Postgresql.PostgresqlMigrator>();
services.AddDatabaseCleaner<ShopDbContext>();
services.AddInitialData<ShopDbContext, TestOrderSeedData>();
csharp
var cleaner = host.Services.GetRequiredService<IDatabaseCleaner<ShopDbContext>>();

// Truncate all tables in FK-safe order (children first)
await cleaner.DeleteAllDataAsync();

// ...or delete + re-run registered IInitialData<T> seeders
await cleaner.ResetAllDataAsync();
  • PostgreSQL: TRUNCATE ... RESTART IDENTITY CASCADE
  • SQL Server: FK-ordered DELETE + DBCC CHECKIDENT
  • SQLite / MySQL / Oracle each get their appropriate strategy

The dependency graph and generated SQL are memoized on first use — zero overhead across hundreds of tests.

📄 Database Reset for Testing

Batched queries — one round trip, many queries

A long-standing EF Core feature request (dotnet/efcore#10879), modeled on Marten's IBatchedQuery:

csharp
await using var batch = context.CreateBatchQuery();

var customersTask = batch.Query(context.Customers.Where(c => c.Name.StartsWith("A")));
var ordersTask    = batch.Query(context.Orders.Where(o => o.Status == "Pending"));

// Single database round trip for both queries
await batch.ExecuteAsync();

var customers = await customersTask;
var orders    = await ordersTask;

On a local SQL Server with 4 keyed lookups per handler, batching delivered a 2.78× speedup (6.92 ms → 2.49 ms); the win grows with query count and network latency.

📄 Batch Queries

JSON columns

OwnsOne().ToJson() mappings are picked up and rendered as jsonb (PostgreSQL) / nvarchar(max) (SQL Server) columns, with nullability driven by IsRequired().

📄 JSON Columns


Try it

bash
dotnet add package Weasel.EntityFrameworkCore

You'll still want a database-specific Weasel package (Weasel.Postgresql or Weasel.SqlServer) for the Migrator implementation.

Whether you're standardizing a mixed Marten + EF Core application on a single migration tool, generating reviewable EF migration files for your DBAs, or just want a fast FK-aware reset in your test suite — the EF Core bridge in Weasel 9.18 is ready.

Weasel is developed and maintained by JasperFx Software.

Released under the MIT License.