|
| 1 | +# Implement server-side CreateRole endpoint + backend validation (roles contract) |
| 2 | + |
| 3 | +## Description |
| 4 | + |
| 5 | +The `admin/roles` contract is the **clean, exemplary** contract (`IRoleDetails` + |
| 6 | +`RoleDetailsValidator`, `CreateRole`/`UpdateRole`/`GetRole` commands) but has **contracts only — |
| 7 | +no server-side implementation**. Task 078 built the client half (`RoleForm` → dispatches a valid |
| 8 | +`CreateRole.Command`); on Save the request reaches the **real web-server**, which has no |
| 9 | +`POST api/Roles` handler, so it returns **405 Method Not Allowed** and the client surfaces the |
| 10 | +generic error toast. This is expected and correct until the server half exists. |
| 11 | + |
| 12 | +Build the **server-side `CreateRole` slice** so the whole contract use case works end-to-end, |
| 13 | +including **backend validation** (the same `RoleDetailsValidator` / `AbstractValidator<IRoleDetails>` |
| 14 | +shape enforced server-side, not just in the browser — never trust the client). |
| 15 | + |
| 16 | +## Evidence / current state |
| 17 | +- Client dispatches `POST api/Roles` (`create-role.cs`: `[RouteMixin("api/Roles", HttpVerb.Post)]`). |
| 18 | +- Aspire console log (web-server) confirms: `POST .../api/Roles - 405` — route answers GET only, no POST. |
| 19 | +- No server-side role files exist under the web-server / web-application (contracts-only feature). |
| 20 | +- `CreateRole.GetMockResponseFactory()` + mock registration already exist (078) — the client works |
| 21 | + under `MOCK_WEB_API`; this task makes it work against the **real** server. |
| 22 | + |
| 23 | +## Checklist |
| 24 | +- [x] Server `CreateRole` Endpoint + Handler — **note:** web-server uses the hand-written |
| 25 | + MVC-style `BaseEndpoint<Command, Response>` pattern (mirrored `TrackEventEndpoint`), not |
| 26 | + FastEndpoints (that's api-server's path). Files: |
| 27 | + `web-server/features/admin/roles/{feature-annotations,create-role-endpoint}.cs`, |
| 28 | + `web-application/features/admin/roles/create-role-handler.cs`. No mapper needed — the |
| 29 | + Command *is* the pipeline message. |
| 30 | +- [x] Backend validation: **zero new code** — `FluentValidationBehavior` (already in the mediator |
| 31 | + pipeline) + `AddValidatorsFromAssemblyContaining<Web.Contracts.IAssemblyMarker>` run the |
| 32 | + contract's composed Validator (shared `RoleDetailsValidator` + `AuthApiRequestValidator`) |
| 33 | + server-side. Integration tests prove both rejection paths (empty Name, empty UserId → 400 |
| 34 | + problem details naming the property). |
| 35 | +- [x] Persistence: **deliberate in-memory stub, documented** in the handler's Design region |
| 36 | + (static `ConcurrentDictionary`) — roles are a template demo feature; swap for a repository |
| 37 | + when real, contract/endpoint unchanged. |
| 38 | +- [x] End-to-end verification via **web-server integration tests** (real host, real pipeline): |
| 39 | + `POST api/Roles` → 200 + non-empty `RoleId`; 18 passed (was 11). Manual RoleForm-in-browser |
| 40 | + check pending healthy local Docker (Aspire) — the integration tests cover the same seam. |
| 41 | +- [x] 078's Save-round-trip checkbox flipped (kanban/done/078, with resolution note). |
| 42 | + |
| 43 | +## Results — beyond the checklist |
| 44 | + |
| 45 | +- **Unblocked by prerequisite fix:** `BaseEndpoint`/`BaseFastEndpoint` constrained |
| 46 | + `TResponse : BaseResponse`, which would have forced `CreateRole.Response` back into the |
| 47 | + `BaseResponse` shape that RFC Decision 5 rejected. Loosened to `where TResponse : class` |
| 48 | + (`Send` uses no `BaseResponse` member; both bases kept aligned). |
| 49 | +- **All roles route warts fixed** (was: blocks 078 Edit/View): `get-role.cs` |
| 50 | + `{RoleId:min(1)}`→`{RoleId:guid}`; `update-role.cs` `api/Role/{RoleId:int}`→ |
| 51 | + `api/Roles/{RoleId:guid}` **+ removed stray `Guid Guid` property**; `delete-role.cs` RPC-style |
| 52 | + `api/DeleteRole`→`api/Roles/{RoleId:guid}` **+ removed hand-declared `required int RoleId`** |
| 53 | + (generator emits the Guid from the route). Serialization test updated (RoleId now `Guid`) — |
| 54 | + the 083 tests caught the contract change, as intended by the tests-first sequencing. |
| 55 | +- Verified: `dev build` 0/0; web-server integration 18 passed; contracts round-trips 7/7; |
| 56 | + analyzer 21/21; sourcegen 14/14. |
| 57 | + |
| 58 | +## Notes |
| 59 | +- **Contract warts to resolve first (or alongside):** `update-role.cs` /`get-role.cs` route uses |
| 60 | + `api/Role/{RoleId:int}` — `RoleId` typed **`int`** in the route but **`Guid`** everywhere else, and |
| 61 | + **singular** `api/Role` vs plural `api/Roles`. This blocks 078's Edit/View modes and should be fixed |
| 62 | + as part of the contracts cleanup. See [[contract-conventions-rfc]] / |
| 63 | + [[077-contracts-compliance-01-nullability-validator-agreement]]. |
| 64 | +- Ties into the open FastEndpoints source-gen rename work (task 053-002: `[RouteMixin]` →`[Route]`); |
| 65 | + keep the new endpoint consistent with whatever attribute naming lands. |
| 66 | +- Follow-up to [[078-add-roleform-demonstrating-idetails-binding-and-shared-validation]] — that task |
| 67 | + proved the client binding + validation; this one completes the round-trip. |
0 commit comments