---
name: ef-core-migration
description: Tạo/sửa/revert EF Core 10 migration cho SOLUTION_ERP. Dùng khi thêm entity mới, thay đổi schema, rollback migration, debug DesignTimeDbContextFactory fail. Đã có 16 migration sẵn (Init → AddTwoStageDeptApprovalAndSmartReject). Snapshot + Designer + Migration 3-file rule bắt buộc commit đủ.
when-to-use:
  - "thêm migration"
  - "EF Core migration"
  - "dotnet ef migrations add"
  - "revert migration"
  - "schema DB update"
  - "DbContext change"
  - "snapshot lỗi"
  - "DesignTimeDbContextFactory"
---

# EF Core Migration — SOLUTION_ERP

> **Context:** .NET 10 + EF Core 10 + SQL Server. DbContext: `ApplicationDbContext` ở `Infrastructure/Persistence/`. Startup: `SolutionErp.Api`.

## Migration history (16 migration hiện có)

| # | Name | Tables added / changed |
|---|---|---|
| 1 | `Init` | 7 Identity tables (AspNetUsers/Roles/UserRoles/...) |
| 2 | `AddMasterData` | Suppliers, Projects, Departments |
| 3 | `AddPermissions` | MenuItems, Permissions |
| 4 | `AddForms` | ContractTemplates, ContractClauses |
| 5 | `AddContractsWorkflow` | Contracts, ContractApprovals, ContractComments, ContractAttachments, ContractCodeSequences |
| 6 | `AddNotifications` | Notifications |
| 7 | `AddWorkflowTypeAssignments` | WorkflowTypeAssignments (legacy admin override) |
| 8 | `AddVersionedWorkflows` | WorkflowDefinitions, WorkflowSteps, WorkflowStepApprovers + `Contracts.WorkflowDefinitionId` FK |
| 9 | `AddContractDetailsAndChangelog` | 7 per-type Details (ThauPhu/GiaoKhoan/NhaCungCap/DichVu/MuaBan/NguyenTacNcc/NguyenTacDv) + ContractChangelogs |
| 10 | `AddMasterCatalogs` | UnitsOfMeasure, MaterialItems, ServiceItems, WorkItems |
| 11 | `AddRoleShortNameAndUserDepartment` | +Role.ShortName + User.DepartmentId/Position (cột thêm, không bảng mới) |
| **12** | **`AddPurchaseEvaluations`** | **10 bảng module Duyệt NCC: PurchaseEvaluations + Suppliers + Details + Quotes + Approvals + Changelogs + Attachments + WorkflowDefinitions/Steps/StepApprovers** |
| **13** | **`AddPurchaseEvaluationCodeSequences`** | **1 bảng `PurchaseEvaluationCodeSequences` (Prefix PK, LastSeq) — atomic sequence cho MaPhieu format `PE/{YYYY}/{A\|B}/{Seq:D3}`** |
| **14** | **`AddBudgets`** | **4 bảng module Ngân sách: Budgets + BudgetDetails + BudgetApprovals + BudgetChangelogs. + nullable FK index `Contracts.BudgetId` & `PurchaseEvaluations.BudgetId`** |
| **15** | **`AddPurchaseEvaluationDepartmentOpinions`** | **1 bảng `PurchaseEvaluationDepartmentOpinions` (UNIQUE PEId+Kind, max 1 row mỗi PeDepartmentKind per phiếu — Phê duyệt/Ccm/MuaHang/SmPm)** |
| **16** | **`AddTwoStageDeptApprovalAndSmartReject`** | **3 bảng `Contract/PurchaseEvaluation/Budget DepartmentApprovals` (UNIQUE TargetId+Phase+Dept+Stage cho 2-stage NV.Review → TPB.Confirm per phòng × phase) + 4 ALTER (`Users.CanBypassReview` bit cho NV bypass + 3 `RejectedFromPhase` int cho smart reject jump-back). Phase 9 — đóng bug "NV duyệt được hết phase" anh Kiệt (FDC) báo. Logic 2-stage trong PurchaseEvaluationWorkflowService chỉ áp PE; HĐ + Budget defer.** |

Total: **55 bảng** dbo + `__EFMigrationsHistory`. Xem `docs/database/schema-diagram.md` ERD đầy đủ.

**Phase 7 pending:**
- `AddPePaymentTermFields` — tách `PurchaseEvaluations.PaymentTerms` JSON thành 6 column riêng
- `AddPeDepartmentOpinions` — thêm 4 field Ý kiến phòng ban (Phê duyệt/CCM/MuaHàng/SM-PM) vào header hoặc table riêng

**Phase 8 pending (Budget refinement):**
- `AddBudgetCodeSequences` — atomic sequence khi format MaNganSach chốt (mirror Contract/PE pattern, hiện Random.Shared)
- `AddBudgetVersionedWorkflow` — nếu Solutions cần admin config UI (3 bảng `BudgetWorkflowDefinitions/Steps/StepApprovers` + `Budgets.WorkflowDefinitionId?`)

**Phase 8 update (2026-04-29 Session 5):**
- Migration 15 `AddPurchaseEvaluationDepartmentOpinions` — 1 bảng riêng (UNIQUE PEId+Kind), max 4 row mỗi phiếu cho 4 phòng ban (Phê duyệt/Ccm/MuaHang/SmPm). UPDATE in-place khi user đổi ý, audit qua Changelog.
- Tests Phase 1-2-3mini-2stage live: `tests/SolutionErp.Domain.Tests/` (54 test policy state machine) + `tests/SolutionErp.Infrastructure.Tests/` (17 test code generator + 6 test PE WF versioning + 6 test PE 2-stage approval Session 9). **Total 83 test pass / ~3s**. CI fail → no deploy.
- CI optimize 3 fix (29/04):
  - Manual checkout bypass github.com (gotcha #39) — fix TCP timeout 21s
  - Path filter docs-only skip (gotcha #41) — commit MD-only KHÔNG trigger CI
  - npm junction cache (gotcha #40) — rolled back, debug session sau
- Migration verify pattern thêm: chạy `dotnet test SolutionErp.slnx` local trước mỗi migration commit để chống regression policy/codegen.

**Phase 7 update (2026-04-28):**
- Migration 14 `AddBudgets` — 4 bảng (Budgets/Details/Approvals/Changelogs) + 2 cột `BudgetId?` thêm vào Contracts & PurchaseEvaluations. Workflow hardcoded `BudgetPolicy.Default` (chưa versioned).
- 14 demo user thật `@solutions.com.vn` qua `SeedDemoUsersAsync` reconcile (PRO 5 + CCM 7 + ISO 1 + CEO 1).

**Phase 6 update (2026-04-24):**
- Enum `PurchaseEvaluationAttachmentPurpose.ComparisonTable = 4` mới (file bảng so sánh tổng).
  Int column, không cần migration.
- `BackfillUserEmailDomainAsync` trong DbInitializer — rename in-place 4 field
  Email/NormalizedEmail/UserName/NormalizedUserName. Idempotent.

## Commands (chạy từ root repo)

### Thêm migration mới

```powershell
# Sau khi chỉnh Domain entity + EF config + DbContext DbSet
dotnet ef migrations add <MigrationName> `
  --project src/Backend/SolutionErp.Infrastructure `
  --startup-project src/Backend/SolutionErp.Api `
  --output-dir Persistence/Migrations
```

**Naming convention:** PascalCase, verb prefix:
- `Add<Thing>` — thêm entity / bảng
- `Update<Thing>` — thay đổi schema
- `Remove<Thing>` — xóa column / table
- `Rename<Thing>` — đổi tên

### Apply migration to DB

```powershell
# Dev: LocalDB SolutionErp_Dev
dotnet ef database update `
  --project src/Backend/SolutionErp.Infrastructure `
  --startup-project src/Backend/SolutionErp.Api

# Hoặc đơn giản: chạy API → DbInitializer tự Migrate
dotnet run --project src/Backend/SolutionErp.Api
```

### Revert 1 migration (rollback)

```powershell
# Rollback về migration trước đó (ví dụ về sau AddWorkflowTypeAssignments)
dotnet ef database update AddWorkflowTypeAssignments `
  --project src/Backend/SolutionErp.Infrastructure `
  --startup-project src/Backend/SolutionErp.Api

# Xóa file migration (3 file, xem dưới)
dotnet ef migrations remove `
  --project src/Backend/SolutionErp.Infrastructure `
  --startup-project src/Backend/SolutionErp.Api
```

### Gen SQL script (review trước khi apply prod)

```powershell
# Script từ 1 migration → latest
dotnet ef migrations script AddWorkflowTypeAssignments `
  --project src/Backend/SolutionErp.Infrastructure `
  --startup-project src/Backend/SolutionErp.Api `
  --output tmp/migration-7-to-8.sql

# Idempotent (check if exists trước mỗi operation)
dotnet ef migrations script --idempotent `
  --project src/Backend/SolutionErp.Infrastructure `
  --startup-project src/Backend/SolutionErp.Api
```

## 3-file rule (BẮT BUỘC commit đủ)

Mỗi `migrations add` sinh **3 file** — phải commit đủ:

```
src/Backend/SolutionErp.Infrastructure/Persistence/Migrations/
├── {Timestamp}_{Name}.cs              ← migration logic (Up/Down)
├── {Timestamp}_{Name}.Designer.cs     ← model snapshot tại thời điểm migration
└── ApplicationDbContextModelSnapshot.cs  ← current model state (được overwrite mỗi migration)
```

**Gotcha #17:** Thiếu `ModelSnapshot.cs` → `migrations add` kế tiếp sẽ sinh duplicate columns. Thiếu `Designer.cs` → không revert được.

## Pitfalls thường gặp

### P1 — DesignTime DbContext resolve fail

**Triệu chứng:** `dotnet ef migrations add` → `Unable to resolve service for type 'DbContextOptions<ApplicationDbContext>'`.

**Nguyên nhân:** EF tooling chạy đứng ngoài runtime DI, cần factory riêng.

**Fix:** Đã có `src/Backend/SolutionErp.Infrastructure/Persistence/DesignTimeDbContextFactory.cs`. Nếu connection string thay đổi → update factory, không chỉ `appsettings.json`.

### P2 — Table rename / column rename gen ra DROP + CREATE

**Triệu chứng:** Migration sinh `DropColumn` + `AddColumn` thay vì `RenameColumn` → mất data.

**Fix:** Sửa migration thủ công sang `migrationBuilder.RenameColumn(...)`. Hoặc dùng `[Column("newname")]` attribute để EF tự detect rename.

### P3 — Query filter (soft delete) khi thêm FK

**Triệu chứng:** Warning `The entity type 'X' has a global query filter but referencing entity 'Y' doesn't. This may lead to inconsistent results`.

**Fix:** Entity reference cũng phải có query filter:
```csharp
builder.HasQueryFilter(x => !x.IsDeleted);
```

Hoặc dùng `.IgnoreQueryFilters()` cho query cần bypass.

### P4 — FK cascade/restrict khác với expectation

**Triệu chứng:** Migration apply OK nhưng DELETE parent → cascade sang child (hoặc ngược lại restrict block).

**Fix:** Explicit config trong `IEntityTypeConfiguration<T>`:
```csharp
builder.HasOne(x => x.Contract)
    .WithMany(c => c.Approvals)
    .HasForeignKey(x => x.ContractId)
    .OnDelete(DeleteBehavior.Cascade);  // hoặc Restrict, SetNull
```

**Case study quan trọng:** `Contracts.WorkflowDefinitionId` → `WorkflowDefinitions.Id` **PHẢI dùng `Restrict`** để protect HĐ cũ khi admin archive version (xem `workflow-contract.md` invariants).

### P5 — Nullable reference type gen column NOT NULL

**Triệu chứng:** Property `public string? Description` → migration gen `NOT NULL`.

**Fix:** Check `builder.HasQueryFilter` chỗ config, hoặc explicit `builder.Property(x => x.Description).IsRequired(false)`.

### P6 — `AddVersionedWorkflows` duplicate seed

**Case study hiện tại:** Nếu thêm migration mới sau `AddVersionedWorkflows`, cẩn thận KHÔNG trigger `DbInitializer.SeedWorkflowDefinitionsAsync` 2 lần. Check `if (!db.WorkflowDefinitions.Any())` trong DbInitializer.

## Workflow khi thêm entity mới (example: add AuditLog)

```
1. Domain/AuditLogs/AuditLog.cs — tạo entity (BaseEntity hoặc AuditableEntity)
2. Infrastructure/Persistence/Configurations/AuditLogConfiguration.cs
   - ToTable("AuditLogs")
   - Unique index trên (EntityType, EntityId) nếu cần
3. Infrastructure/Persistence/ApplicationDbContext.cs
   - public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
4. Application/Common/Interfaces/IApplicationDbContext.cs
   - DbSet<AuditLog> AuditLogs { get; }
5. dotnet ef migrations add AddAuditLogs
6. Review file .cs sinh ra → OK
7. dotnet ef database update → LocalDB apply
8. Test: dotnet run → check bảng được tạo
9. Commit [CLAUDE] Domain+Infra: add AuditLog entity + migration
10. ⚠️ UPDATE docs/database/schema-diagram.md (ERD + migration table)
11. UPDATE docs/STATUS.md nếu là work lớn
```

## Apply prod (VPS)

```powershell
# Trên VPS, sau khi deploy code:
# Option 1: auto migrate — DbInitializer.MigrateAsync() chạy khi API startup
# Option 2: manual script
dotnet ef migrations script <LastApplied> --idempotent --output migrate.sql
# Review migrate.sql → chạy qua sqlcmd:
sqlcmd -S .\SQLEXPRESS -d SolutionErp -U vrapp -P <pw> -i migrate.sql
```

**Prod safety:**
- Backup trước: `scripts/backup-sql.ps1`
- Dry-run idempotent script local trước
- Test rollback plan: script from-N-to-M-1 sẵn

## Code pointers

- `src/Backend/SolutionErp.Infrastructure/Persistence/ApplicationDbContext.cs` — DbSet cho 55 bảng (16 migration)
- `src/Backend/SolutionErp.Infrastructure/Persistence/DesignTimeDbContextFactory.cs` — EF tooling factory
- `src/Backend/SolutionErp.Infrastructure/Persistence/DbInitializer.cs` — seed + warn + migrate runtime + backfill (idempotent reconcile pattern)
- `src/Backend/SolutionErp.Infrastructure/Persistence/Configurations/` — IEntityTypeConfiguration<T> per entity
- `src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs` — interface Application layer

## Related

- `docs/database/database-guide.md` — conventions + migration workflow chi tiết
- `docs/database/schema-diagram.md` — **ERD 55 bảng** + §11 PE module + §12 Budget module + §13 PEDeptOpinions + §14 DepartmentApprovals (Mig 16)
- `docs/gotchas.md` #7, #17, #38 — migration pitfalls + Identity 4-field rename
