Files
solution-erp/.claude/skills/ef-core-migration/SKILL.md
pqhuy1987 21ee36390e [CLAUDE] Docs+Skill: chốt Session 16 wrap-up — drastic refactor flat workflow (Chunk D)
Session 16 (2026-05-08) docs/skill/memory wrap-up:

STATUS.md:
- Last updated Session 16 (2 commit Chunk A+B)
- Phase summary count (20→21 mig, 96→77 test, 57→55 bảng)
- Recently Done row Session 16 chi tiết (drastic refactor hoàn tất)
- Phase enum simplified semantic post-Mig 21

HANDOFF.md:
- TL;DR Session 16 prepend với 2 chunk + Chunk C SKIP rationale
- Per-chunk implementation chi tiết (Domain + Mig 21 + Service + Tests
  + FE Designer)
- 8 cảnh báo Session 17+: UAT live test / old data migration / Sample
  seed / Budget N-stage / schema-diagram / skill refresh / tests flat /
  Hard blockers Ops

migration-todos.md: Phase 9 + Session 16 block 2 chunk done + 7 defer task

Session log NEW `2026-05-08-0500-drastic-refactor-flat-workflow.md`:
- Bối cảnh resume từ S15 defer
- Spec implementation (Phase enum, state machine, schema Mig 21)
- Per-chunk Chunk A + Chunk B detail
- Chunk C skip rationale
- Memory `feedback_drastic_refactor_scope` validation:
  scope estimate 30% accurate (3h actual vs 10h conservative)
- Plan organization sau S16

Skill ef-core-migration:
- description + heading: 20→21 migration
- + Mig 21 row "RefactorWorkflowToFlatModel" với detail (4 ALTER + 2
  ALTER + DROP TABLE × 2 + DROP COLUMN × 2 + restore simple unique × 2)
- Total 57→55 bảng (-2 InnerStep tables)
- Tests: 96→77 (drop 19 legacy)

CLAUDE.md root:
- Migration count 20→21
- DB tables 57→55
- Workflow flat description thay N-stage description

docs/rules.md §7: 96→77 test (Mig 21 simplified)

Memory `project_solution_erp.md`:
- Add "Tổng sau session 16" block với drastic refactor details

🎉 Session 16 wrap-up. Cumulative since session start (S15 wrap-up
`38d10b7`): 4 commit (S15 wrap-up was final S15) + 3 commit Session 16
(A `dbb0089` + B `88a5be1` + D current).

Defer Session 17+ priority:
1. UAT live test workflow flat (3 phòng × N cấp realistic)
2. Old PE/HĐ legacy phase data migration
3. Sample data seed (Task 2 carry-over)
4. Hard blockers Ops

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:16:32 +07:00

16 KiB
Raw Blame History

name, description, when-to-use
name description when-to-use
ef-core-migration 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ó 21 migration sẵn (Init → RefactorWorkflowToFlatModel). Snapshot + Designer + Migration 3-file rule bắt buộc commit đủ.
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: ApplicationDbContextInfrastructure/Persistence/. Startup: SolutionErp.Api.

Migration history (21 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.
17 AddManualBudgetFieldsToPeAndContract 4 ALTER (PE + HĐ × BudgetManualName nvarchar(200) + BudgetManualAmount decimal(18,2)) — manual budget fallback khi user không link Budget entity approved. KHÔNG XOR với BudgetId, cả 2 cùng null OK. Carry-forward pe.BudgetManualName/Amount → contractCreateContractFromEvaluation. Phase 9 — Session 11 (2026-05-07).
18 AddPeWorkflowInnerStepsAndPositionLevel N-stage workflow PE — 1 CREATE TABLE PurchaseEvaluationWorkflowStepInnerSteps (Order, DepartmentId, PositionLevel, Name, SlaDays, IsRequired) + 2 ALTER (Users.PositionLevel int? 1=NV/2=PP/3=TP + PEDeptApproval.InnerStepId Guid?) + 3 IX + FK Cascade Step / Restrict Dept+InnerStep. Phase 9+ — Session 12 (2026-05-07).
19 AlterPeDeptApprovalsUniqueFilteredForInnerSteps Filtered unique split: drop UNIQUE cũ Mig 16 → 2 filtered: legacy WHERE InnerStepId IS NULL (Stage Review/Confirm) + N-stage WHERE InnerStepId IS NOT NULL (per inner step). Tránh conflict 2 inner step cùng dept Stage=Confirm. Session 12.
20 AddContractWorkflowInnerStepsAndAlterDeptApprovalUnique N-stage workflow Contract mirror PE Mig 18+19 — GỘP 1 migration: CREATE TABLE WorkflowStepInnerSteps + ALTER ContractDeptApproval.InnerStepId + DropIndex old + Recreate filtered legacy/N-stage + 3 IX + FK. Session 13 (2026-05-07).
21 RefactorWorkflowToFlatModel 🎯 DRASTIC REFACTOR (Session 16, 2026-05-08): bỏ phase enum legacy, dùng ChoDuyet=10 đơn nhất + currentStepIndex tracking. Workflow flat (Phòng × Cấp + Approvers). 4 ALTER (PE/Contract +CurrentWorkflowStepIndex/RejectedAtStepIndex) + 2 ALTER (WorkflowStep +DepartmentId/PositionLevel PE+Contract) + DROP TABLE × 2 (PE+Contract WorkflowStepInnerSteps Mig 18+20) + DROP COLUMN InnerStepId × 2 + DROP filtered indexes Mig 19/20 + RESTORE simple unique non-filtered × 2.

Total: 55 bảng dbo + __EFMigrationsHistory (Mig 21 drop 2 InnerStep tables, restore Mig 16 simple unique). Xem docs/database/schema-diagram.md ERD đầy đủ.

N-stage workflow pattern (Mig 18-20 — Session 12-13)

Architecture decision đáng ghi cho session sau:

Pattern: Mỗi WorkflowStep cha (= 1 phase) cấu hình động chuỗi InnerSteps con theo Department × PositionLevel với Order sequential. Approver cần khớp DeptId + PositionLevel + Order tiếp theo chưa duyệt. CanBypassReview cho user level cao hơn skip cấp dưới cùng dept.

Per-module migration (PE first, mirror sang Contract sau):

  • PE: 2 migration tách (Mig 18 schema + Mig 19 alter index) — vì index issue phát hiện sau khi schema đã commit
  • Contract: 1 migration GỘP (Mig 20) — gộp CREATE TABLE + ALTER + DropIndex + Recreate filtered. Cleaner cho mirror.

Filtered unique trick: legacy 2-stage rows (Mig 16, Stage=Review/Confirm) + N-stage rows (per InnerStep) cùng table *DepartmentApprovals → cần unique split:

  • UX_*_Phase_Dept_Stage WHERE InnerStepId IS NULL (legacy, prevent duplicate Review/Confirm cùng phase × dept)
  • UX_*_Phase_InnerStep WHERE InnerStepId IS NOT NULL (N-stage, prevent duplicate per inner step)

Backward compat 100%: workflow no InnerSteps configured → service fallback legacy 2-stage logic Mig 16. Data legacy InnerStepId=null vẫn enforce unique cũ qua filtered index.

Budget defer: Budget chưa có versioned WorkflowDefinition entity (hardcoded BudgetPolicy.Default). Để mirror N-stage Budget cần migration AddBudgetVersionedWorkflow trước (4 bảng + ALTER Budget.WorkflowDefinitionId?). Defer cho user quyết riêng.

Phase 7 pending:

  • AddPePaymentTermFields — tách PurchaseEvaluations.PaymentTerms JSON thành 6 column riêng (Session 11+++++ đã thay UI Workspace <Textarea> JSON → <Select> 8 preset, BE schema vẫn nvarchar(max). Migration tách field defer cho khi BE cần aggregate/filter từng terms 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 (đã làm Mig 15 — bảng riêng UNIQUE PEId+Kind)

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 live (Mig 21 drastic refactor drop legacy 19): tests/SolutionErp.Domain.Tests/ (54 test policy state machine) + tests/SolutionErp.Infrastructure.Tests/ (17 test code generator + 6 test PE WF versioning). Total 77 test pass / ~3s. Drop 19 legacy (PE 2-stage S9 + PE N-stage S12 + Contract N-stage S13 + PE Reject S14) — Mig 21 simplify model, write new tests cho flat workflow flow khi UAT bug.
  • 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

# 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

# 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)

# 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)

# 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.csmigrations 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 addUnable 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:

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>:

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.WorkflowDefinitionIdWorkflowDefinitions.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)

# 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 per entity
  • src/Backend/SolutionErp.Application/Common/Interfaces/IApplicationDbContext.cs — interface Application layer
  • docs/database/database-guide.md — conventions + migration workflow chi tiết
  • docs/database/schema-diagram.mdERD 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