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 its Up() contains only the new change — never a CreateTable for anything that already exists.
  • Entity classes are scaffolded from the real DB, not hand-written. Command:
    dotnet ef dbcontext scaffold "$LegacyConnStr" Microsoft.EntityFrameworkCore.SqlServer \
      --project src/Manex.Data --startup-project src/Manex.Web \
      --output-dir Entities --context MnxEFContext --no-onconfiguring --force
    Handwritten AspnetUsers / AspnetMembership under src/Manex.Data/Entities/ are the Phase 0 stand-in until we can point at a real staging BcomMX to scaffold from.
  • __EFMigrationsHistory bootstrap — when the first real migration lands, its idempotent script will CREATE TABLE __EFMigrationsHistory IF NOT EXISTS automatically. No manual seeding needed as long as no migration ever tries to create a legacy table.

Consequences#

  • No accidental DROP TABLE in production. Migrations never claim ownership of a legacy table they didn’t create.
  • The Docker Compose db service 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 behind docker-compose.legacy.yml which omits the db service and expects ConnectionStrings__eManEx to be pointed at 192.168.9.111\SQLDEV2017 (or a personal restore of a BcomMX backup).
  • Test isolation — integration tests already use Microsoft.EntityFrameworkCore.InMemory via TestWebAppFactory. 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_status column, say), we:
    1. Scaffold the current live schema into MnxEFContextModelSnapshot.cs.
    2. Modify the entity class.
    3. dotnet ef migrations add … — the delta is AddColumn, not CreateTable.
    4. Ship the migration through the same CI pipeline that ships code.
  • Rollback for schema changes requires either a paired Down() that reverses the delta, or a scripted rollback in TSQL/. 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.