# Code Generation Plan — U2 Data Durability **This plan is the single source of truth for Code Generation of U2.** Generation executes exactly these steps in order; no step is added or skipped during execution. --- ## Unit Context | Aspect | Detail | |---|---| | **Unit** | U2 Data Durability | | **Round** | R1 (with U1 Hosting & Serving) | | **Workspace root** | `K:\Development\Projects\SlpModularCms` | | **Project type** | Brownfield — existing structure retained, files modified in place | | **Requirements** | FR-11, FR-12 | | **Components** | C-05 Data Protection, C-06 keys table, C-07 migration runner, U2 portion of C-16 | | **Business rules** | BR-U2-01 … BR-U2-18 | | **Depends on** | Nothing. U1 and U2 are mutually independent | | **Depended on by** | U6 — durability must land before the first automated deploy | | **New database entities** | One: the Data Protection keys table, owned by `ApplicationDbContext` | ### Requirement traceability | Requirement | Implemented by steps | |---|---| | FR-11 — automatic `ApplicationDbContext` migration at startup | 4, 5, 6 | | FR-12 — persistent Data Protection key ring | 1, 2, 3, 5, 6 | ### Why this unit exists Nothing here is user-visible. Its whole purpose is that U6's atomic release switch **cannot** silently destroy schema state or Master↔slave trust. Two failure modes are being closed, both of which would otherwise appear only after the first production deploy and present as something else entirely. --- ## Generation Steps ### Step 1: Package reference - [x] Modify `src/SlpModularCms.Core/SlpModularCms.Core.csproj` to add `Microsoft.AspNetCore.DataProtection.EntityFrameworkCore` version **10.0.9**, matching the existing 10.0.x line ### Step 2: Keys table on the Core context - [x] Modify `src/SlpModularCms.Core/Data/ApplicationDbContext.cs`: - [x] Implement `IDataProtectionKeyContext` - [x] Add `DbSet DataProtectionKeys` - [x] Leave every existing entity configuration untouched ### Step 3: Data Protection registration - [x] Create `src/SlpModularCms.Core/Hosting/DataProtectionExtensions.cs` with `AddCmsDataProtection()` - [x] Configure `PersistKeysToDbContext()` (BR-U2-01) - [x] Set the application discriminator to a **fixed constant in code** (BR-U2-02) — not configurable, not derived from any path - [x] Leave key lifetime at the framework default of 90 days (BR-U2-05) - [x] Document in code why the discriminator is a constant: the default derives from the content root path, which changes on every atomic release switch ### Step 4: Startup migration runner - [x] Create `src/SlpModularCms.Core/Hosting/DatabaseMigrationExtensions.cs` with `MigrateCoreDatabase()` - [x] Apply `ApplicationDbContext` migrations before the application accepts traffic (BR-U2-09) - [x] Classify failures (BR-U2-11, BR-U2-12): retry **connection** failures with increasing delay up to a bounded number of attempts; fail **migration** failures immediately with no retry - [x] Log the reason before failing, with diagnostic context but **no** connection string, credentials or secrets (BR-U2-14) - [x] Propagate the exception when retries are exhausted or the failure is a migration failure, so the process does not start (BR-U2-13) ### Step 5: Remove the conflicting module registrations - [x] Modify `src/SlpModularCms.Modules.Availability/AvailabilityModule.cs` — remove `services.AddDataProtection()` - [x] Modify `src/SlpModularCms.Modules.Master/MasterModule.cs` — remove `services.AddDataProtection()` - [x] Leave every other registration in both modules unchanged; they continue consuming `IDataProtector` (BR-U2-04) *This is the § 5.1 conflict. Module registration runs **after** the host's, so these bare calls would override the persistent key store. `IDataProtector` resolves either way, so the defect would surface only after the first release switch as slave API keys that no longer decrypt — presenting as a network fault between Master and slave.* ### Step 6: Host composition - [x] Modify `src/SlpModularCms.Api/Program.cs` — call `AddCmsDataProtection()` **before** `orchestrator.RegisterModuleServices(...)` (BR-U2-03), and `MigrateCoreDatabase()` after `builder.Build()` and before `orchestrator.UseModules(app)` (BR-U2-10) - [x] Modify `src/SlpModularCms.Api.Slave/Program.cs` — the same two calls in the same positions ### Step 7: Core migration - [x] Generate the EF Core migration for the keys table into `src/SlpModularCms.Core/Migrations/` - [x] Verify the migration is purely additive — no dropped or narrowed columns, so redeploying an earlier release stays safe (BR-U2-16) ### Step 8: Data Protection unit tests - [x] Create `src/SlpModularCms.Core.Tests/Hosting/DataProtectionExtensionsTests.cs`: - [x] The persistent key store **survives module registration** — the highest-value assertion in this unit, since registration alone passes in both the broken and fixed cases - [x] The application discriminator is the fixed constant, not a path-derived value - [x] A protected value round-trips across a simulated content-root change - [x] Neither module registers Data Protection, so the conflict cannot be reintroduced by a future change ### Step 9: Migration runner unit tests - [x] Create `src/SlpModularCms.Core.Tests/Hosting/DatabaseMigrationExtensionsTests.cs`: - [x] A connection failure is retried - [x] A migration failure is **not** retried and fails immediately - [x] Retry exhaustion propagates - [x] Failure logging contains no connection string or credentials ### Step 10: Documentation - [x] Create `aidlc-docs/features/gitea-deployment-workflow/construction/u2-data-durability/code/generation-summary.md` — files created and modified, decisions taken, and any deviation from this plan - [x] Record the two operational constraints that are enforced by documentation rather than code, for later inclusion in the Operations deployment instructions: the keys table must **never** be pruned (BR-U2-06), and only one instance may migrate a given database at a time (BR-U2-17) ### Step 11: Build and test verification (automatic) - [x] `dotnet build SlpModularCms.sln -c Release` - [x] `dotnet test` for `SlpModularCms.Core.Tests`, `SlpModularCms.Modules.Availability.Tests` and `SlpModularCms.Modules.Master.Tests` - [x] Fix any failure directly and re-run until green - [x] Record the outcome for the completion message --- ## Files Touched ### Created | Path | Purpose | |---|---| | `src/SlpModularCms.Core/Hosting/DataProtectionExtensions.cs` | Persistent key ring registration | | `src/SlpModularCms.Core/Hosting/DatabaseMigrationExtensions.cs` | Startup migration with failure classification | | `src/SlpModularCms.Core/Migrations/*_AddDataProtectionKeys.cs` | Keys table migration | | `src/SlpModularCms.Core.Tests/Hosting/DataProtectionExtensionsTests.cs` | Tests | | `src/SlpModularCms.Core.Tests/Hosting/DatabaseMigrationExtensionsTests.cs` | Tests | ### Modified | Path | Change | |---|---| | `src/SlpModularCms.Core/SlpModularCms.Core.csproj` | Add the Data Protection EF Core package | | `src/SlpModularCms.Core/Data/ApplicationDbContext.cs` | Implement `IDataProtectionKeyContext`, add the keys set | | `src/SlpModularCms.Modules.Availability/AvailabilityModule.cs` | Remove `AddDataProtection()` | | `src/SlpModularCms.Modules.Master/MasterModule.cs` | Remove `AddDataProtection()` | | `src/SlpModularCms.Api/Program.cs` | Data Protection registration and startup migration | | `src/SlpModularCms.Api.Slave/Program.cs` | Same | **Brownfield rule**: every file above that exists is modified in place. No parallel copies. --- ## Risk Notes for the Executor | Risk | Mitigation in this plan | |---|---| | A test that merely asserts "Data Protection is registered" passes in both the broken and fixed cases | Step 8 asserts the **resulting configuration**, not the registration | | The discriminator silently reverting to the path-derived default | Step 8 asserts the constant explicitly | | The new migration being non-additive and breaking rollback | Step 7 verifies additivity | | Startup migration masking a genuine migration fault by retrying it | Step 4 classifies failures; Step 9 asserts the classification | | Both hosts must still start | Step 11 builds the whole solution; the composed startup is verified at the phase-level Build and Test stage, where the Slave — which has no test project — is started | --- ## Out of Scope for U2 - Static content, health endpoint, availability-gate changes — U1 - Security headers — U3 - Sentry, Umami, frontend configuration — U4 - Anything under `.gitea/` — U5 and U6 - Certificate-based key encryption — deferred as DEV-05 follow-up - A distributed migration lock — Q4 = C, handled by documented operational constraint