Adds SlpModularCms.Api.Slave for local master/slave dev testing (Unit 1)

Relocates ModuleOrchestrator, ServiceCollectionExtensions, and
ApiPrefixConvention from SlpModularCms.Api into SlpModularCms.Core.Hosting
so a new Master-less SlpModularCms.Api.Slave host project (ports 5285/7222)
can share the same bootstrap code without duplicating it. This lets a
developer run a master instance and a slave instance side by side locally
to test the master/slave connection, without touching the existing
master/slave protocol itself.

Relocates the two orchestrator/convention test files from
Modules.Identity.Tests to Core.Tests, dropping an incidental
ProjectReference to SlpModularCms.Api that existed only for those tests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-04 02:07:35 +02:00
co-authored by Claude Sonnet 5
parent 8072025e0b
commit 274946dbff
36 changed files with 1380 additions and 14 deletions
@@ -0,0 +1,24 @@
# Requirements Clarification — Follow-up
Your Question 1 answer combines two different concerns, and I want to scope this feature correctly before writing requirements.
## Ambiguity 1: Scope of the module-loading change
You answered Q1 with: *"For testing purposes a separate project will suffice, but when building the production builds later I want to be able to build the base version of the API and add the modules as DLL-files. Can we make that happen here as well?"*
Today, `SlpModularCms.Modules.Master` is a `ProjectReference` in `SlpModularCms.Api.csproj`, so it's always compiled straight into the API's own build output — `ModuleOrchestrator` then discovers it there via DLL-scanning. A true "base API + drop in module DLLs later" production model would mean:
- Removing the `ProjectReference` to `Modules.Master` (and, by the same logic, to `Modules.Identity`/`Modules.Availability`) from `SlpModularCms.Api.csproj`.
- Introducing a separate "Modules" output/probing folder that the API loads DLLs from at startup (instead of its own bin folder), populated by copying built module DLLs into it (a packaging/deployment concern).
- Deciding what happens to module-specific EF Core migrations, DI wiring, and versioning across independently-built modules.
This is a meaningfully bigger architectural change than "add a config flag to skip the Master module for a local slave test instance" — it touches build/packaging (`.csproj` structure, CI/CD artifacts) well beyond local dev tooling.
### Clarification Question 1
How do you want to handle this?
A) Keep this feature scoped to local dev only: a separate `SlpModularCms.Api.Slave` test host project (no `Modules.Master` reference) for running master+slave locally. Capture the "base API + drop-in module DLLs for production" idea as a new tech-debt-backlog / future-feature item to design properly on its own later.
B) Expand this feature to also implement the production drop-in-DLL loading model now (restructure `SlpModularCms.Api.csproj` to not directly reference module projects, add a module-probing folder loaded at runtime, update `ModuleOrchestrator` accordingly) — understanding this is a larger, riskier change touching the deployment model, not just local dev.
C) Do the config-driven `Modules:Disabled` toggle from the original option A instead (one shared build, no separate project, no production refactor) — simplest for local dev, defers the production plugin model entirely.
X) Other (please describe after [Answer]: tag below)
[Answer]: A
@@ -0,0 +1,62 @@
# Requirements Clarification Questions — Local Dev Master/Slave Setup
Context found while investigating the codebase: `SlpModularCms.Api` discovers modules by scanning its own build output folder for `SlpModularCms.Modules.*.dll` (`ModuleOrchestrator.DiscoverModules`) and loads every module DLL it finds — there is currently no config-driven way to load the API without `SlpModularCms.Modules.Master`. To run a "slave-only" instance locally, `ModuleOrchestrator` needs a small change: skip registering/activating a module if it's named in a new config setting (e.g. `Modules:Disabled`), so the same build output can serve both an instance with the Master module active and one without.
Please answer each question below by filling in your choice after the `[Answer]:` tag.
## Question 1
How should the "master" vs "slave" instance be toggled?
A) Config-driven module disable list read from `appsettings`/`appsettings.local.json` (e.g. `"Modules": { "Disabled": ["Master"] }`), checked by `ModuleOrchestrator` before registering/activating a module. One shared build; two different config files/launch profiles select the mode.
B) Separate ASP.NET Core launch profiles only, no code change — accepting that both profiles will always load the Master module since it's a project reference (this would NOT actually give you a Master-less slave).
C) A separate host project (`SlpModularCms.Api.Slave`) with its own `.csproj` that simply omits the `SlpModularCms.Modules.Master` project reference.
X) Other (please describe after [Answer]: tag below)
[Answer]: X, For testing purposes a separate project will suffice, but when building the production builds later I want to be able to build the base version of the API and add the modules as DLL-files. Can we make that happen here as well?
## Question 2
Should the master and slave instance use separate local databases (so slave-side data like registrations/users doesn't collide with master-side data)?
A) Yes — separate connection strings/database names (e.g. `SlpModularCmsLocal_Master` and `SlpModularCmsLocal_Slave`), same SQL Server instance, via `appsettings.local.json` per profile.
B) No — both instances share the exact same local database.
C) Use SQLite per instance instead of SQL Server, to make the setup fully self-contained without a shared SQL Server.
X) Other (please describe after [Answer]: tag below)
[Answer]: A
## Question 3
Which ports should the two instances use?
A) Master keeps the existing ports (`http://localhost:5284`, `https://localhost:7221`); slave gets a new pair (`http://localhost:5285`, `https://localhost:7222`).
B) Let me specify custom ports (describe after [Answer]: tag below).
X) Other (please describe after [Answer]: tag below)
[Answer]: A
## Question 4
How do you want to start both instances locally?
A) Two named `dotnet run --launch-profile <name>` invocations (one per terminal) — profiles added to the existing `SlpModularCms.Api/Properties/launchSettings.json`.
B) A helper script (e.g. PowerShell) that launches both in the background with one command.
C) Both A and B — launch profiles for IDE debugging (e.g. Rider/VS "Multiple startup projects" or `dotnet run`), plus a convenience script for quick CLI use.
X) Other (please describe after [Answer]: tag below)
[Answer]: A
## Question 5
Should the frontend (React app) also be runnable against the slave instance, or is the slave purely a backend API target (no need to browse it via the CMS frontend)?
A) Backend-only — slave just needs to be reachable over HTTP for the master-to-slave connection (registration + polling/push); Scalar/OpenAPI at the slave's own URL is enough to poke it manually if needed.
B) The frontend should also be runnable against the slave (e.g. via a `.env.slave` or a second Vite dev server config), so its admin UI is separately reachable too.
X) Other (please describe after [Answer]: tag below)
[Answer]: B
## Question 6
Once both instances are running, do you want a documented/scripted way to actually register the slave under the master (so the "connection" is set up end-to-end), or is wiring that up manually through the existing "Add CMS Instance" UI flow sufficient?
A) Manual is fine — document the steps (start both, open master frontend, use the existing Add CMS Instance dialog with the slave's local URL) in a short README/runbook.
B) Provide a scripted/seeded way (e.g. a dev-only endpoint or seed data) to pre-register the slave automatically on startup, skipping the manual UI step.
X) Other (please describe after [Answer]: tag below)
[Answer]: A, because I want to test the functionality of the "Add CMS Instance" UI flow.
@@ -0,0 +1,80 @@
# Requirements — Local Dev Master/Slave Setup
## Intent Analysis
- **User Request**: Enable running two local instances of the CMS backend simultaneously — one with `SlpModularCms.Modules.Master` loaded (the existing full CMS, acting as master) and one without it (acting purely as a slave/target) — on two different ports, so the master↔slave connection can be tested end-to-end locally.
- **Request Type**: New Feature (developer tooling / local run configuration)
- **Scope Estimate**: Multiple components — new host project (`SlpModularCms.Api.Slave`), solution/build changes, `appsettings`/`launchSettings` for both instances, frontend dev-server configuration for pointing at either instance.
- **Complexity Estimate**: Moderate — no new business logic; the master/slave protocol itself (`Modules.Master`'s `CmsInstanceService`/`SlaveApiClient` and `Modules.Availability`'s `MasterController`/`MasterAvailabilityService`) already exists and is unchanged. Complexity comes from correctly separating build/run configuration for two instances without duplicating orchestration logic.
## Background (from codebase investigation)
- `SlpModularCms.Api`'s `Program.cs` does not directly reference `Modules.Master`. Instead, `Infrastructure/ModuleOrchestrator.cs` discovers modules at startup by scanning its own build output directory for `SlpModularCms.Modules.*.dll` and instantiating any `IModule` implementation found — currently every module the API project references (`Core`, `Availability`, `Identity`, `Master`) always ends up in that output folder via `ProjectReference`s in `SlpModularCms.Api.csproj`.
- The master/slave connection mechanism already exists and needs no changes:
- **Slave side** (present in every instance, since `Modules.Availability` is always loaded): `MasterController` + `MasterAvailabilityService` + `MasterApiKeyProtector` accept and validate a per-instance API key pushed by a master.
- **Master side** (only in `Modules.Master`): `CmsInstanceService` + `SlaveApiClient` + `ApiKeyProtector` manage registered CMS instances and talk to them.
- The existing frontend "Add CMS Instance" dialog (from `master-cms-module`) is the existing UI flow for registering a slave under a master.
- Frontend reads its backend URL from `VITE_API_BASE_URL` (`frontend/src/lib/config.ts`), sourced from a Vite env file (`.env.local`, `.env.example`), evaluated once per dev server process. Vite dev server currently runs on a fixed port (5173, `vite.config.ts`).
- Backend CORS allowed origins are configured per-instance under `Cors:AllowedOrigins` in `appsettings.Development.json` / `appsettings.local.json`.
## Decisions from Clarification
1. **Instance separation mechanism**: A new host project, `SlpModularCms.Api.Slave`, with its own `.csproj` referencing `SlpModularCms.Core`, `SlpModularCms.Modules.Identity`, and `SlpModularCms.Modules.Availability` — but **not** `SlpModularCms.Modules.Master`. Because `ModuleOrchestrator` discovers modules purely by scanning its own build output, omitting the `Master` project reference is sufficient to produce a slave-only instance; no config-driven module toggle is needed for this feature.
2. **Production plugin-loading model is explicitly out of scope**: the user separately wants a future production model where the API is built once without module project references and modules are dropped in as standalone DLLs post-build. That is a distinct, larger architectural change (build/packaging/CI, not local dev tooling) and is **not** part of this feature. Capture it as a follow-up idea (see "Out of Scope" below).
3. **Database isolation**: Master and slave instances use separate local databases (separate connection strings/database names), same local SQL Server.
4. **Ports**: Master keeps existing ports (`http://localhost:5284`, `https://localhost:7221`). Slave gets a new pair (`http://localhost:5285`, `https://localhost:7222`).
5. **Launch method**: Two `dotnet run --launch-profile <name>` invocations (one per terminal/process) — no background helper script required.
6. **Frontend**: The frontend must also be runnable against the slave instance (its own dev server / env configuration), not just the master — so the slave's admin UI is separately reachable for manual testing.
7. **Establishing the connection**: No scripted/seeded auto-registration. The user will manually register the slave under the master using the existing "Add CMS Instance" UI flow, specifically to exercise/validate that flow end-to-end.
## Functional Requirements
### FR-1: Slave-only backend host project
- Add `SlpModularCms.Api.Slave`, a new ASP.NET Core Web API project in `src/`, added to the solution.
- Bootstraps the same way as `SlpModularCms.Api` (core infrastructure, CORS, rate limiting, controllers, module discovery/orchestration, exception handling, Scalar in Development) — but its `.csproj` references `Core`, `Modules.Identity`, and `Modules.Availability` only (no `Modules.Master` reference), so `ModuleOrchestrator` never discovers/loads the Master module for this instance.
- Avoid duplicating the bootstrap/orchestration code (`ModuleOrchestrator`, `ServiceCollectionExtensions`, `ApiPrefixConvention`) between `SlpModularCms.Api` and `SlpModularCms.Api.Slave` — extract shared pieces into a location both projects can reference without pulling in `Modules.Master` (see NFR-1).
### FR-2: Distinct local run configuration per instance
- `SlpModularCms.Api` (master) keeps its existing `launchSettings.json` profiles and ports (5284 / 7221).
- `SlpModularCms.Api.Slave` gets its own `launchSettings.json` with profiles on ports 5285 (HTTP) and 7222 (HTTPS).
- Each project has its own `appsettings.json` / `appsettings.Development.json` / `appsettings.local.json` (gitignored) with:
- A distinct `ConnectionStrings:DefaultConnection` (separate database name/catalog) so master and slave don't share data.
- `Cors:AllowedOrigins` covering whichever frontend origin(s) will point at that instance.
- The slave instance does not need `MasterModule` configuration (that section is only meaningful where `Modules.Master` is loaded).
- Both instances can be started independently via `dotnet run --launch-profile <name>` from their respective project directories.
### FR-3: Frontend configurable against either instance
- Add a way to run the existing frontend against the slave instance in addition to the master, without needing two separate frontend codebases:
- A second Vite env file (e.g. `.env.slave.local`, gitignored like `.env.local`) setting `VITE_API_BASE_URL` to the slave's HTTPS URL (`https://localhost:7222`).
- A second npm script (e.g. `dev:slave`) that runs Vite with that env mode on a different dev-server port (e.g. 5174), so both frontend instances can run side by side if needed.
- Update `.env.example` and/or `frontend/README.md` to document both modes.
### FR-4: Documentation for the local master/slave workflow
- Add a short runbook (e.g. `frontend/README.md` and/or a root-level `docs`/`README` section, or a dedicated markdown file referenced from the main README) describing:
1. How to start the master backend + master frontend.
2. How to start the slave backend (+ optionally slave frontend) on the alternate ports.
3. How to use the master frontend's existing "Add CMS Instance" dialog to register the local slave (what URL/API key to use) and confirm the connection works (e.g. status shows as connected/healthy).
## Non-Functional Requirements
### NFR-1: No logic duplication between host projects
- The master and slave host projects must share bootstrap/orchestration code (module discovery, DI wiring for core infrastructure, controller conventions) rather than maintaining two copies that can drift. Exact extraction mechanism (e.g. a small shared class library, or a shared partial/extension file linked into both projects) is a Functional/Application Design decision, not fixed here.
### NFR-2: Isolation
- Master and slave instances must not share a database, so testing against one cannot corrupt or interfere with the other's data.
### NFR-3: Local-only scope
- No changes to the actual master/slave communication protocol, production deployment, or CI/CD pipeline. This is strictly a local development convenience.
### NFR-4: Secrets hygiene
- Slave's `appsettings.local.json` (and any new gitignored env files) must follow the existing pattern — never committed, following `dotnet-appsettings` conventions already used by `SlpModularCms.Api`.
## Out of Scope
- **Production drop-in module DLL loading**: Building the API once without module project references and loading modules from an external "Modules" folder dropped in post-build/at-deploy. This is a distinct future initiative (build/packaging/CI concern) explicitly deferred by the user during clarification — to be captured as a new backlog/feature item when picked up, not designed as part of this feature.
- **Automated/scripted slave registration**: The user explicitly wants to exercise the existing "Add CMS Instance" UI flow manually; no seed data or dev-only registration endpoint will be added.
- Any change to `Modules.Master`, `Modules.Availability`, or the master/slave communication protocol itself.
## Summary
This feature adds a second, Master-less ASP.NET Core host project (`SlpModularCms.Api.Slave`) plus per-instance run configuration (ports, databases, CORS) and frontend tooling, so the developer can run a master CMS instance (full stack, port 5284/7221) and a slave-only instance (no Master module, port 5285/7222) side by side locally, and manually connect them via the existing "Add CMS Instance" UI flow to validate the master/slave integration end-to-end. The production "plugin-style" module loading idea raised during clarification is explicitly out of scope and noted for future consideration.