diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 38d8c72..eb071e2 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -26,6 +26,7 @@ export default withMermaid( ] }, { text: 'EF Core', link: '/efcore/' }, + { text: 'Blog', link: '/blog/' }, { text: 'Resources', items: [ @@ -136,11 +137,20 @@ export default withMermaid( { text: 'Table Mapping', link: '/efcore/table-mapping' }, { text: 'Migrations', link: '/efcore/migrations' }, { text: 'Migration Generation', link: '/efcore/migration-generation' }, + { text: 'Schema Mapping Customization', link: '/efcore/schema-customization' }, { 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' } ] + }, + { + text: 'Blog', + collapsed: true, + items: [ + { text: 'All Posts', link: '/blog/' }, + { text: 'Weasel Grows Up on EF Core', link: '/blog/efcore-9.18' } + ] } ], diff --git a/docs/blog/efcore-9.18.md b/docs/blog/efcore-9.18.md new file mode 100644 index 0000000..4d49a12 --- /dev/null +++ b/docs/blog/efcore-9.18.md @@ -0,0 +1,371 @@ +--- +title: "Weasel Grows Up on EF Core: Schema Parity, Migration Generation, and Multi-Tenant Hooks" +date: 2026-07-19 +tags: [weasel, efcore, migrations, critter-stack] +--- + +# Weasel Grows Up on EF Core + +[Weasel](https://weasel.jasperfx.net) is the low-level database schema management and +migration engine extracted from [Marten](https://martendb.io) โ€” 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](https://weasel.jasperfx.net/efcore/) + +--- + +## 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](https://weasel.jasperfx.net/efcore/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 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 | + +### 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 -> 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("id").AsPrimaryKey(); +table.AddColumn("name").NotNull(); + +var baseline = EfSchemaSnapshot.FromSchemaObjects(new ISchemaObject[] { table }, options); +var json = baseline.ToJson(); + +// ... later: the model changed +table.AddColumn("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](https://weasel.jasperfx.net/efcore/migration-generation) +ยท ๐Ÿ“„ [Mixed EF + Critter Stack Apps](https://weasel.jasperfx.net/efcore/migration-coexistence) + +--- + +## 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("tenant_id").AsPrimaryKey(); +partitionRegistry.AddColumn("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](https://weasel.jasperfx.net/efcore/schema-customization) + +--- + +## The supporting cast + +### FK-aware database cleaner for tests + +Inspired by [Respawn](https://github.com/jbogard/respawn) and Marten's `ResetAllData()`, +the cleaner discovers tables from `DbContext` metadata, resolves FK ordering, and generates +provider-specific truncation SQL: + +```csharp +services.AddDbContext(o => o.UseNpgsql("Host=localhost;Database=mydb")); +services.AddSingleton(); +services.AddDatabaseCleaner(); +services.AddInitialData(); +``` + +```csharp +var cleaner = host.Services.GetRequiredService>(); + +// Truncate all tables in FK-safe order (children first) +await cleaner.DeleteAllDataAsync(); + +// ...or delete + re-run registered IInitialData 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](https://weasel.jasperfx.net/efcore/database-cleaner) + +### Batched queries โ€” one round trip, many queries + +A long-standing EF Core feature request ([dotnet/efcore#10879](https://github.com/dotnet/efcore/issues/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](https://weasel.jasperfx.net/efcore/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](https://weasel.jasperfx.net/efcore/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. + +- ๐Ÿ“„ [EF Core docs](https://weasel.jasperfx.net/efcore/) +- ๐Ÿ™ [JasperFx/weasel on GitHub](https://github.com/JasperFx/weasel) +- ๐Ÿ’ฌ [Critter Stack Discord](https://discord.gg/WMxrvegf8H) + +*Weasel is developed and maintained by [JasperFx Software](https://jasperfx.net).* diff --git a/docs/blog/index.md b/docs/blog/index.md new file mode 100644 index 0000000..102eab9 --- /dev/null +++ b/docs/blog/index.md @@ -0,0 +1,8 @@ +# Blog + +News, deep dives, and release notes for Weasel. + +## Posts + +- [**Weasel Grows Up on EF Core: Schema Parity, Migration Generation, and Multi-Tenant Hooks**](/blog/efcore-9.18) โ€” *2026-07-19* + How the `Weasel.EntityFrameworkCore` package matured across the recent releases: byte-for-byte schema parity with EF Core migrations, EF Core migration file generation, and the new schema-mapping customization hook. diff --git a/docs/efcore/index.md b/docs/efcore/index.md index 50ac673..ca0d081 100644 --- a/docs/efcore/index.md +++ b/docs/efcore/index.md @@ -2,6 +2,10 @@ The `Weasel.EntityFrameworkCore` NuGet package bridges Entity Framework Core's `DbContext` model to Weasel's schema management infrastructure. This allows you to use Weasel's migration tooling, delta detection, and CLI commands with schemas defined through EF Core's fluent API. +::: tip New in 9.18 +For a tour of the recent EF Core improvements โ€” schema parity, EF Core migration generation, and the schema-mapping customization hook โ€” see the blog post [Weasel Grows Up on EF Core](/blog/efcore-9.18). +::: + ## Installation ```bash diff --git a/docs/efcore/schema-customization.md b/docs/efcore/schema-customization.md new file mode 100644 index 0000000..ec4d374 --- /dev/null +++ b/docs/efcore/schema-customization.md @@ -0,0 +1,100 @@ +# Schema Mapping Customization + +The [table mapping](/efcore/table-mapping) is deliberately provider-neutral, and that neutral seam can't express everything a real integration needs โ€” PostgreSQL table partitioning, provider-specific index methods, or extra bookkeeping tables that must be migrated alongside the entity tables. `EfSchemaMappingCustomization` is the escape hatch. + +The motivating case is **Wolverine's conjoined multi-tenancy**: it needs to attach Weasel-managed tenant partitioning to the tables mapped from tenant-scoped EF entity types, *and* inject the partition control/registry tables into the same migration so everything is created together. + +## The hook + +`EfSchemaMappingCustomization` has two parts: + +| Member | Type | Purpose | +|---|---|---| +| `CustomizeTable` | `Action?` | Called for every table mapped from an EF entity type, **after** the standard mapping. Decorate the table โ€” attach partitioning, adjust indexes, opt into drift detection โ€” including by downcasting to the concrete provider `Table`. | +| `AdditionalObjects` | `IReadOnlyList` | Extra schema objects (partition control/registry tables, sequences, ...) migrated **ahead of** the mapped entity tables, so anything the tables depend on exists first. | + +The same `IEntityType` / `ITable` pair is presented on **every** mapping pass โ€” both `CreateDatabase` and each `CreateMigrationAsync` โ€” so your customization stays applied consistently across delta detection and DDL generation. + +## Usage + +Pass a customization to `CreateMigrationAsync`. Here a tenant-scoped entity type gets Weasel-managed `LIST` partitioning, and a registry table is contributed to the migration ahead of the entity tables: + + + +```cs +// 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("tenant_id").AsPrimaryKey(); +partitionRegistry.AddColumn("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); + +if (migration.Migration.Difference != SchemaPatchDifference.None) +{ + await migration.ExecuteAsync(AutoCreate.CreateOrUpdate, ct); +} +``` +snippet source | anchor + + +The `CustomizeTable` delegate downcasts the neutral `ITable` to the concrete `Weasel.Postgresql.Tables.Table` to reach `PartitionByList(...)` โ€” the same technique used for Npgsql `HasMethod("gin")` indexes or descending sort order that the neutral seam doesn't model. See [PostgreSQL partitioning](/postgresql/partitioning) for the partitioning APIs. + +## Where it plugs in + +Customization-aware overloads exist on all three entry points; the original no-customization overloads simply forward with `null`: + +- `IServiceProvider.CreateMigrationAsync(context, customization, cancellation)` โ€” detect and apply changes. +- `IServiceProvider.CreateDatabase(context, customization, identifier?)` โ€” build an `IDatabaseWithTables`. +- `DbContextExtensions.GetSchemaObjectsForMigration(context, migrator, customization)` โ€” the lower-level list of schema objects (sequences + additional objects + mapped tables, in dependency order). + + + +```cs +var customization = new EfSchemaMappingCustomization +{ + CustomizeTable = (entityType, table) => + { + // e.g. opt individual tables into drift detection + table.DetectColumnDrift = true; + } +}; + +// The same customization is applied on every mapping pass +var database = serviceProvider.CreateDatabase(dbContext, customization); +``` +snippet source | anchor + + +## Ordering guarantees + +Contributed `AdditionalObjects` are always emitted **before** the mapped entity tables, and (as with the default flow) model sequences come before everything. That means a partition control table, a lookup table, or a sequence referenced by a mapped table's default is guaranteed to exist by the time the entity tables are created. + +## Notes + +- `AdditionalObjects` that implement `ITable` are registered with the `IDatabase` in `CreateDatabase`, so they participate in delta detection like any other Weasel table. Objects that are not tables flow through the `GetSchemaObjectsForMigration` / migration path. +- Because the hook hands you the real `ITable`, anything you can do to a Weasel table โ€” [drift detection](/efcore/table-mapping#column-drift-detection), check constraints, extra indexes โ€” is available here per table. +- The customization is a plain object with no framework coupling; construct it wherever you compose your migration and pass it through. diff --git a/src/DocSamples/EfCoreCustomizationSamples.cs b/src/DocSamples/EfCoreCustomizationSamples.cs new file mode 100644 index 0000000..071113d --- /dev/null +++ b/src/DocSamples/EfCoreCustomizationSamples.cs @@ -0,0 +1,81 @@ +using JasperFx; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata; +using Weasel.Core; +using Weasel.EntityFrameworkCore; +using Weasel.Postgresql; +using ITable = Weasel.Core.ITable; +using PgTable = Weasel.Postgresql.Tables.Table; + +namespace DocSamples; + +// A marker interface an application (e.g. Wolverine's conjoined multi-tenancy) +// might use to opt entity types into tenant partitioning +public interface ITenantScoped +{ +} + +public class EfCoreCustomizationSamples +{ + private IServiceProvider serviceProvider = null!; + private DbContext dbContext = null!; + private CancellationToken ct; + + public async Task customize_mapped_tables_and_contribute_objects() + { + #region sample_efcore_schema_mapping_customization + // 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("tenant_id").AsPrimaryKey(); + partitionRegistry.AddColumn("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); + + if (migration.Migration.Difference != SchemaPatchDifference.None) + { + await migration.ExecuteAsync(AutoCreate.CreateOrUpdate, ct); + } + #endregion + } + + public void customize_when_building_a_database() + { + #region sample_efcore_customization_create_database + var customization = new EfSchemaMappingCustomization + { + CustomizeTable = (entityType, table) => + { + // e.g. opt individual tables into drift detection + table.DetectColumnDrift = true; + } + }; + + // The same customization is applied on every mapping pass + var database = serviceProvider.CreateDatabase(dbContext, customization); + #endregion + } +}