ADR 0004 — Database migration strategy#
Status: Accepted (Phase 0, 2026-07-07)
Context#
The legacy Cube 1.x database (BcomMX_01_04 on 192.168.9.111\SQLDEV2017) has ~800 tables, dozens of views, hundreds of stored procedures, and — critically — live production data we are not throwing away. The .NET 8 rewrite consumes the same database during the migration.
Phase 0 initially shipped a generative baseline migration (20260703170911_InitialBaseline) whose Up() called CreateTable("aspnet_Users") and CreateTable("aspnet_Membership"). Applying that against the real database would fail:
There is already an object named 'aspnet_Membership' in the database.
That is the wrong model for our situation. The Migration Roadmap already states the rule:
Database-first EF Core — Scaffold EF Core 8 models from the existing MSSQL baseline database; evolve schema via EF migrations.
The generative baseline violated that rule. This ADR makes the stance explicit.
Decision#
- Phase 0 ships no migrations.
src/Manex.Data/Migrations/is empty. Entity classes describe existing tables; the schema is treated as external. db.Database.Migrate()is never called at startup for Phase 0. The connection is opened lazily on first query.- New migrations, when they arrive, are always forward-only deltas. The first one is added with
dotnet ef migrations add <Name>while pointing at a scaffolded snapshot that already reflects the legacy schema, so itsUp()contains only the new change — never aCreateTablefor anything that already exists. - Entity classes are scaffolded from the real DB, not hand-written. Command:Handwritten
dotnet ef dbcontext scaffold "$LegacyConnStr" Microsoft.EntityFrameworkCore.SqlServer \ --project src/Manex.Data --startup-project src/Manex.Web \ --output-dir Entities --context MnxEFContext --no-onconfiguring --forceAspnetUsers/AspnetMembershipundersrc/Manex.Data/Entities/are the Phase 0 stand-in until we can point at a real staging BcomMX to scaffold from. __EFMigrationsHistorybootstrap — when the first real migration lands, its idempotent script willCREATE TABLE __EFMigrationsHistory IF NOT EXISTSautomatically. No manual seeding needed as long as no migration ever tries to create a legacy table.
Consequences#
- No accidental
DROP TABLEin production. Migrations never claim ownership of a legacy table they didn’t create. - The Docker Compose
dbservice is a dev sandbox only — a fresh, empty SQL Server instance for isolated tests and local play. Real work happens against the legacy DB, gated behinddocker-compose.legacy.ymlwhich omits thedbservice and expectsConnectionStrings__eManExto be pointed at192.168.9.111\SQLDEV2017(or a personal restore of a BcomMX backup). - Test isolation — integration tests already use
Microsoft.EntityFrameworkCore.InMemoryviaTestWebAppFactory. They do not touch a real database. - When a real schema change is needed (Phase 2’s SO/RMA API adds a
sales_order_new_statuscolumn, say), we:- Scaffold the current live schema into
MnxEFContextModelSnapshot.cs. - Modify the entity class.
dotnet ef migrations add …— the delta isAddColumn, notCreateTable.- Ship the migration through the same CI pipeline that ships code.
- Scaffold the current live schema into
- Rollback for schema changes requires either a paired
Down()that reverses the delta, or a scripted rollback inTSQL/. Both are per-migration decisions documented at the time.
When we would reverse this#
If Cube 2.0 is ever cutover to a new database instance — a clean rebuild rather than an in-place evolution — we would switch back to code-first migrations at that point. That decision belongs to Phase 8 or the post-migration cleanup.