Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7ed3103
feat(efcore): Weasel model → EF MigrationOperation translation layer
jeremydmiller Jul 18, 2026
850ac00
feat(efcore): C# migration file emitter + stub DbContext
jeremydmiller Jul 18, 2026
9dbe74c
feat(efcore): incremental migrations — serialized snapshot + differ
jeremydmiller Jul 18, 2026
bb1f24a
feat(efcore): db-ef-migration add | script | baseline CLI command
jeremydmiller Jul 18, 2026
f8686dc
feat(efcore): inverted schema-comparison validation harness
jeremydmiller Jul 18, 2026
4d6741a
docs(efcore): EF Core migration generation documentation
jeremydmiller Jul 18, 2026
c9ccfd1
fix(tests): resolve SampleGenerated path from output dir, not CallerF…
jeremydmiller Jul 18, 2026
2bc42e6
Merge branch 'feat/ef-migration-emitter' into feat/ef-incremental-mig…
jeremydmiller Jul 18, 2026
b904d45
Merge branch 'feat/ef-migration-emitter' into feat/ef-migration-cli
jeremydmiller Jul 18, 2026
e66dcbe
Merge branch 'feat/ef-migration-emitter' into feat/ef-inverted-harness
jeremydmiller Jul 18, 2026
c8e6ef8
Merge branch 'feat/ef-migration-emitter' into feat/ef-migration-docs
jeremydmiller Jul 18, 2026
ca5cf04
Merge remote-tracking branch 'origin/master' into feat/ef-incremental…
jeremydmiller Jul 18, 2026
b78bfc3
Merge branch 'feat/ef-incremental-migrations' into feat/ef-migration-cli
jeremydmiller Jul 18, 2026
68d02a3
Merge branch 'feat/ef-migration-cli' into feat/ef-inverted-harness
jeremydmiller Jul 18, 2026
0cd3162
Merge branch 'feat/ef-inverted-harness' into feat/ef-migration-docs
jeremydmiller Jul 18, 2026
adc1eaf
Merge remote-tracking branch 'origin/master' into feat/ef-inverted-ha…
jeremydmiller Jul 19, 2026
c196cd6
Merge branch 'feat/ef-inverted-harness' into feat/ef-migration-docs
jeremydmiller Jul 19, 2026
22a6bb0
Merge remote-tracking branch 'origin/master' into feat/ef-migration-docs
jeremydmiller Jul 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ src/
│ └── Tables/ # Table handling with Oracle-specific features
├── Weasel.Sqlite/ # SQLite implementation (NEW!)
│ └── Tables/ # Table handling with JSON support
├── Weasel.EntityFrameworkCore/ # EF Core bridge: DbContext -> Weasel mapping,
│ │ # EF migration file generation (translation layer,
│ │ # emitter, snapshot differ, db-ef-migration CLI)
│ └── CommandLine/ # db-ef-migration add | script | baseline
└── *Tests/ # Test projects for each library
```

Expand Down
14 changes: 14 additions & 0 deletions EFCORE_IMPROVEMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,20 @@ on PostgreSQL and SQL Server, and participates in delta detection with
canonicalized expression comparison), and HiLo / `HasSequence` sequences
(mapped through `Migrator.CreateSequence`).

## EF Core migration generation (epic #371)

The reverse direction landed as a phased epic: Weasel schema objects (from any
`IDatabase`) translate into EF Core `MigrationOperation` lists
(`MigrationOperationTranslation`), render as compilable attribute-only
migration files + a stub DbContext with a relocated history table
(`EfMigrationFileEmitter`), diff incrementally against a serialized JSON
snapshot or a live database (`EfSchemaSnapshot` / `EfSnapshotDiffer`), and ship
through the `db-ef-migration add | script | baseline` command. Validated by an
inverted comparison harness that compiles the generated migrations with Roslyn,
applies them through the real EF runtime, and requires catalog parity plus a
`None` Weasel delta. Docs: `docs/efcore/migration-generation.md` and
`docs/efcore/migration-coexistence.md`.

## Test infrastructure

- **New CI workflow** `ci-build-efcore.yml` — the EF Core test project
Expand Down
2 changes: 2 additions & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ export default withMermaid(
{ text: 'Overview', link: '/efcore/' },
{ text: 'Table Mapping', link: '/efcore/table-mapping' },
{ text: 'Migrations', link: '/efcore/migrations' },
{ text: 'Migration Generation', link: '/efcore/migration-generation' },
{ text: 'Mixed EF + Critter Stack Apps', link: '/efcore/migration-coexistence' },
{ text: 'JSON Columns', link: '/efcore/json-columns' },
{ text: 'Database Reset for Testing', link: '/efcore/database-cleaner' },
{ text: 'Batch Queries', link: '/efcore/batch-queries' }
Expand Down
58 changes: 58 additions & 0 deletions docs/efcore/migration-coexistence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Mixed EF Core + Critter Stack Applications

The [migration generation](/efcore/migration-generation) feature is built for applications that combine an EF Core application model with Marten, Wolverine, or Polecat storage in **one database**. This page covers how the pieces coexist.

## Two migration streams, one database

A mixed application has two independent migration streams:

- **Your application's own `DbContext`** with its entities, its migrations, and its `__EFMigrationsHistory` in the default location.
- **The generated Weasel stub context** carrying the critter-stack schema, with its history table **relocated into the critter-stack schema** (e.g. `marten.__EFMigrationsHistory`), so the two never collide.

Both apply cleanly to the same database; each `dotnet ef` invocation targets one stream via `--context`:

```bash
dotnet ef database update --context AppDbContext
dotnet ef database update --context MartenSchemaDbContext
```

At runtime, register both contexts — the stub context needs nothing beyond a connection string and its history table location (the registration snippet is generated into the stub's XML docs):

```csharp
services.AddDbContext<AppDbContext>(o => o.UseNpgsql(connectionString));
services.AddDbContext<MartenSchemaDbContext>(o =>
o.UseNpgsql(connectionString,
m => m.MigrationsHistoryTable("__EFMigrationsHistory", "marten")));
```

Multiple `IDatabase` registrations (any mix of Marten + Polecat + Wolverine) produce **per-database migration sets and stub contexts** — select which one to generate for with the same `--database` flag the other `db-*` commands use. For multi-tenancy with a database per tenant, generate **one** migration set and apply the same artifacts against each tenant connection string.

## The single-owner rule

**A table is managed by exactly one migration stream.** The generated Weasel migrations own the critter-stack tables; your application context owns its entity tables. Nothing may own a table twice — otherwise the two streams fight over its shape.

If your application context *maps* critter-stack-managed tables (for querying with EF), exclude them from its migrations:

```csharp
modelBuilder.Entity<OrderProjection>()
.ToTable("order_projections", "marten",
t => t.ExcludeFromMigrations());
```

## EF Core projections — the round trip

The full round-trip story for EF-projected documents:

1. Your projection `DbContext` defines the projection tables.
2. [`MapToTable` / `GetSchemaObjectsForMigration`](/efcore/table-mapping) turns that model into Weasel schema objects registered with the `IDatabase`.
3. `db-ef-migration add` then includes the projection tables in the generated migrations automatically — one stream owns them end to end.

Ownership guidance: when a projection table enters the `IDatabase` this way, the **generated Weasel stream owns it** — mark it `ExcludeFromMigrations` in the projection context (which continues to be the query surface). This keeps the single-owner rule intact while both EF (queries) and the critter stack (writes) use the table.

## Verification

The test suite proves coexistence and parity in both directions:

- The dual-schema comparison harness: EF creates a schema → Weasel's delta detection reports `None`, and vice versa.
- The inverted harness: Weasel-defined schemas → generated migrations compiled and applied via the real EF runtime → catalog parity against a Weasel-created schema **and** `SchemaMigration.DetermineAsync` reporting `None`.
- Coexistence scenarios: two generated migration sets (separate schemas and history tables) applying cleanly to one database, and a second application context alongside the stub context.
184 changes: 184 additions & 0 deletions docs/efcore/migration-generation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
# EF Core Migration Generation

Weasel can generate **EF Core migration files from its own schema model** — the reverse direction of [table mapping](/efcore/table-mapping). Instead of Weasel applying schema changes itself (`db-patch` / `db-apply`), it emits standard, compilable EF Core migration artifacts that your team applies with the tools it already knows: `dotnet ef database update`, idempotent SQL scripts, and migration bundles.

## When to use which flow

| | Weasel-native (`db-patch` / `db-apply`) | EF migration generation (`db-ef-migration`) |
|---|---|---|
| Schema is applied by | Weasel at startup or CLI | EF Core toolchain (`dotnet ef`, bundles, scripts) |
| Change history | none (delta against live DB) | versioned migration files + `__EFMigrationsHistory` |
| DBA review artifact | patch SQL file | migration `.cs` files / idempotent script |
| Best for | dev loops, Marten-style auto-migration | teams standardizing on EF migrations for deployment |

Both flows read the same source of truth: **`IDatabase.AllObjects()`** — Marten system tables, Wolverine envelope storage, Polecat event storage, and [EF-projection tables](/efcore/table-mapping) all flow through one door.

## The generated artifacts

`db-ef-migration add <Name>` (or `EfMigrationGenerator.AddAsync`) writes three kinds of files:

1. **Migration classes** (`<timestamp>_<Name>.cs`) — attribute-only `Migration` subclasses carrying `[DbContext]` and `[Migration]` attributes with real `Up()` **and** `Down()` bodies over the public `MigrationBuilder` surface. There is deliberately no `BuildTargetModel` body: the empty target model is fully supported by the EF toolchain (runtime `Migrate()`, CLI update, `--idempotent` scripts, and bundles were all verified end-to-end on EF 9 and EF 10).
2. **A stub `DbContext`** (once per `IDatabase`) — no entities; provider configured; the `__EFMigrationsHistory` table **relocated into the critter-stack schema** so it never collides with your application's own EF context; the EF 9+ `PendingModelChangesWarning` suppressed; plus an `IDesignTimeDbContextFactory` reading the `WEASEL_EF_CONNECTION` environment variable so the EF CLI works without an application host.
3. **A schema snapshot** (`weasel-schema-snapshot.json`) — Weasel's analog of EF's `ModelSnapshot`: design-time JSON written beside the migrations and never compiled. It is the baseline the next `add` diffs against.

## Getting started

Generate the first migration for the selected `IDatabase` (same `--database` selection UX as `db-patch`):

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

Programmatically:

<!-- snippet: sample_efgen_add_migration -->
<a id='snippet-sample_efgen_add_migration'></a>
```cs
// IDatabase is the source of all schema objects — Marten system
// tables, Wolverine envelope storage, EF projection tables, ...
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)
```
<sup><a href='https://github.com/JasperFx/weasel/blob/master/src/DocSamples/EfCoreMigrationSamples.cs#L14-L30' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_efgen_add_migration' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Compile the generated files into a project that references the EF provider package, then apply them exactly like any EF migration:

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

Idempotent scripts and bundles work the same way:

```bash
dotnet ef migrations script --idempotent --context MyStoreSchemaDbContext -o migrations.sql
dotnet ef migrations bundle --context MyStoreSchemaDbContext
```

## Incremental migrations

The next `db-ef-migration add <Name>` diffs the current model against the snapshot **entirely in memory** — no live database, no shadow container — and emits an incremental migration (`AddColumn` / `AlterColumn` / `DropColumn`, index and foreign-key recreation, primary-key changes, sequence changes) with a real reverse-ordered `Down()`. Migration ids are `yyyyMMddHHmmss_Name` with a monotonicity guard, since EF orders migrations by plain string sort of the id.

<!-- snippet: sample_efgen_snapshot_diff -->
<a id='snippet-sample_efgen_snapshot_diff'></a>
```cs
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();

// the serialized snapshot is Weasel's analog of EF's ModelSnapshot:
// design-time JSON written beside the migrations, never compiled
var baseline = EfSchemaSnapshot.FromSchemaObjects(new ISchemaObject[] { table }, options);
var json = baseline.ToJson();

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

// diff entirely in memory — no live database, no shadow container
var incremental = EfSnapshotDiffer.Diff(EfSchemaSnapshot.FromJson(json), target, options);
// incremental.UpOperations -> AddColumn tenant_id
// incremental.DownOperations -> DropColumn tenant_id
```
<sup><a href='https://github.com/JasperFx/weasel/blob/master/src/DocSamples/EfCoreMigrationSamples.cs#L60-L84' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_efgen_snapshot_diff' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Pass `--against-database` to use the secondary **live-database baseline mode**: Weasel's own delta detection runs against the actual database and the resulting migration SQL (and rollback SQL) is wrapped in `Sql()` operations. This mode handles everything Weasel can migrate, including the cases the snapshot diff deliberately refuses:

<!-- snippet: sample_efgen_live_database_diff -->
<a id='snippet-sample_efgen_live_database_diff'></a>
```cs
// the secondary mode: let Weasel's own delta detection diff against
// the actual database, and wrap the migration SQL in Sql() operations.
// Handles everything Weasel can migrate — including partition deltas
// and function changes the snapshot diff refuses.
var operations = await EfSnapshotDiffer.DiffAgainstDatabaseAsync(database);
```
<sup><a href='https://github.com/JasperFx/weasel/blob/master/src/DocSamples/EfCoreMigrationSamples.cs#L89-L95' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_efgen_live_database_diff' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Adopting an existing database

For a database that already has the schema (a running Marten/Wolverine application), record the generated migrations as applied without executing anything — the EF-sanctioned baselining technique:

```bash
dotnet run -- db-ef-migration baseline
```

<!-- snippet: sample_efgen_baseline -->
<a id='snippet-sample_efgen_baseline'></a>
```cs
// adopt a pre-existing database: record every generated migration file
// as already applied (history rows only, nothing is executed)
var recorded = await EfMigrationGenerator.BaselineAsync(
database,
new EfMigrationGenerationOptions { Directory = "WeaselMigrations" });
```
<sup><a href='https://github.com/JasperFx/weasel/blob/master/src/DocSamples/EfCoreMigrationSamples.cs#L100-L106' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_efgen_baseline' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

Afterwards `dotnet ef database update` reports nothing pending, and future incremental migrations apply on top.

## The translation layer

Under the CLI sits a public API: Weasel schema objects translate into EF `MigrationOperation` instances with **raw store type strings everywhere**, so the DDL EF generates matches Weasel's own byte-for-byte — identity columns become the proper provider annotations, computed columns become `ComputedColumnSql`, cascade actions map onto `ReferentialAction` (with SQL Server's `Restrict` ≡ `NO ACTION` normalization), and schemas get `EnsureSchema` operations.

<!-- snippet: sample_efgen_translate_table -->
<a id='snippet-sample_efgen_translate_table'></a>
```cs
var table = new PgTable("app.orders");
table.AddColumn<int>("id").AsPrimaryKey();
table.AddColumn<string>("name").NotNull();

// translate Weasel schema objects into EF Core MigrationOperation
// instances — raw store types everywhere, so the DDL EF generates
// matches Weasel's own
var options = new MigrationOperationTranslationOptions(EfMigrationProvider.PostgreSql)
{
Migrator = new PostgresqlMigrator()
};

var operations = new ISchemaObject[] { table }.ToMigrationOperations(options);
var downOperations = new ISchemaObject[] { table }.ToDropMigrationOperations(options);

// render as a compilable, attribute-only migration file
var migration = EfMigrationFileEmitter.EmitMigration(
"AddOrders", operations, downOperations,
new EfMigrationEmissionOptions("AppSchemaDbContext"));
```
<sup><a href='https://github.com/JasperFx/weasel/blob/master/src/DocSamples/EfCoreMigrationSamples.cs#L35-L55' title='Snippet source file'>snippet source</a> | <a href='#snippet-sample_efgen_translate_table' title='Start of snippet'>anchor</a></sup>
<!-- endSnippet -->

## Limitations and raw-SQL fallbacks

Everything EF cannot model routes through `migrationBuilder.Sql(...)` blocks carrying Weasel's own DDL, so these work from day one:

- **PostgreSQL table partitioning** (RANGE/LIST/HASH and the managed strategies) — partitioned tables are detected automatically and emitted as raw DDL (the Npgsql EF provider has no partitioning model).
- **PL/pgSQL functions, SQL Server stored procedures and table types**.
- **Expression indexes**, and **SQL Server unique indexes without a filter** (EF's SqlServer generator would otherwise add a spurious `WHERE ... IS NOT NULL` filter, because an attribute-only migration has no model to prove the columns non-nullable).

Deliberate boundaries:

- `dotnet ef migrations add` / `remove` are **not supported against the stub context** — Weasel authors the migrations; the EF scaffolder needs the model snapshot the stub deliberately doesn't have. Use `db-ef-migration add`.
- Changed raw-SQL objects (a partition layout change, a rewritten function body) are refused by the snapshot diff with guidance — generate that migration with `--against-database` or author it by hand.
- Renames are not inferred from the model (Weasel carries no rename intent); today a rename diffs as drop + add.
- v1 providers are **PostgreSQL and SQL Server**. SQLite is out: its ALTER-emulation rebuilds tables from the migration's target model, which attribute-only migrations don't carry.
- The `PendingModelChangesWarning` suppression baked into the stub context is defensive: with no `ModelSnapshot` at all the EF 9+ pending-changes check has nothing to fire on, but the suppression protects anyone who later adds entities to the same context.
7 changes: 6 additions & 1 deletion docs/efcore/migrations.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Migrations

Weasel's EF Core integration provides methods to detect schema differences and apply migrations using the same delta-detection engine that powers Weasel's core migration infrastructure.
Weasel's EF Core integration supports schema migration in **two directions**:

1. **Weasel applies the schema** (this page) — the EF Core model is mapped into Weasel schema objects and Weasel's delta-detection engine detects and applies changes directly, exactly like it does for Marten.
2. **Weasel generates EF Core migration files** — the reverse: Weasel's schema model (including everything an `IDatabase` carries) is emitted as standard, compilable EF Core migrations that your team applies with `dotnet ef database update`, idempotent scripts, or bundles. See [EF Core Migration Generation](/efcore/migration-generation) and the [coexistence guide](/efcore/migration-coexistence).

The rest of this page covers the first direction: detecting schema differences and applying migrations using the same delta-detection engine that powers Weasel's core migration infrastructure.

## Creating a Migration

Expand Down
Loading
Loading