# CQRS/Mediator Refactor — Design ## Goal Replace the `IXService`/`XService` application layer (9 interfaces, ~38 methods, injected directly into controllers) with MediatR commands/queries and handlers. Controllers depend on `ISender` only; each HTTP action sends one request and returns the result. No behavior change — this is a structural refactor, not a feature change. ## Why Current pattern is service-per-aggregate with broad interfaces (`IProjectService` has 7 methods covering reads, writes, search). CQRS splits each operation into its own request type, so each handler has one job, and cross-cutting concerns (validation, logging, auth) can later be added as MediatR pipeline behaviors without touching every service class. ## Package - Add `MediatR` (latest v13) to `mws.application.csproj`. - Note: MediatR (Jimmy Bogard) moved to a commercial license for non-OSS/non-trivial use starting v13, mirroring AutoMapper (already used in this repo). Explicitly accepted by the project owner for this refactor; AutoMapper is out of scope and left as-is. ## Scope All 7 request-bearing service modules, converted in one pass (not piloted): | Module | Old interface | Methods | |---|---|---| | Auth | `IAuthService` | `LoginAsync` | | Accounts | `IAccountService` | `GetAccountsAsync`, `CreateAccountAsync`, `UpdateAccountAsync`, `DeleteAccountAsync`, `ResetPasswordAsync` | | Roles | `IRoleService` | `GetRolesAsync`, `GetRoleAsync`, `CreateRoleAsync`, `UpdateRoleAsync`, `DeleteRoleAsync` | | Projects | `IProjectService` | `GetProjectsAsync`, `GetProjectAsync`, `CreateProjectAsync`, `UpdateProjectAsync`, `ArchiveProjectAsync`, `GetOverviewAsync`, `SearchAsync` | | ProjectMembers | `IProjectMemberService` | `GetMembersAsync`, `AddMemberAsync`, `UpdateMemberDocumentPermissionsAsync`, `RemoveMemberAsync` | | Documents | `IDocumentService` | `GetTreeAsync`, `GetAsync`, `CreateAsync`, `UpdateAsync`, `MoveAsync`, `DeleteAsync`, `SearchAsync` | | Tasks | `ITaskService` | `GetTasksAsync`, `GetAsync`, `CreateAsync`, `UpdateAsync`, `DeleteAsync`, `SearchAsync` | **Excluded from conversion** (stay plain injected services, unchanged): - `IPermissionService` (`EnsureAsync`, `GetMenuAsync`) — `EnsureAsync` is called from inside `AccountService`'s methods (soon `Account*Handler`s) and directly in `RolesController`. Converting it to a mediator request would mean handlers calling `Send()` on other handlers, which MediatR's own docs call out as an anti-pattern. `GetMenuAsync` stays alongside it for consistency (one small interface, one caller: `MenuController`). - `ITokenService`, `IPasswordHasher` — infrastructure helpers consumed by handlers, not themselves endpoint operations. ## Conventions Per module folder (e.g. `mws.application/Projects/`): - `Commands/` — one file per write operation: `CreateProject.cs` containing `public record CreateProjectCommand(Guid UserId, string Name, string? Description) : IRequest;` and `public class CreateProjectHandler(IUnitOfWork uow, IMapper mapper) : IRequestHandler` directly below it in the same file. Void operations (`Archive`, `Delete`, `Remove`) use `IRequest` (no generic parameter) instead of `IRequest`. - `Queries/` — same colocation pattern for reads: `GetProjects.cs`, `SearchProjects.cs`, etc. - `Contracts.cs` (DTOs) stays exactly as-is. - Handler bodies are the old service method bodies moved verbatim — same `IUnitOfWork` calls, same exceptions (`NotFoundException`, `ForbiddenException`, `BadRequestException`), same authorization checks (`GetMemberRoleAsync`, `EnsureMemberAccessAsync`, etc.). No logic changes. - The `Guid userId`/`actorUserId` parameter that every old service method took explicitly becomes the first property on the command/query record (handlers have no ambient HTTP context). - Delete `IXService.cs` and `XService.cs` for every converted module once its controller no longer references the interface. ## Controllers Each controller's constructor param changes from `IXService x` to `ISender sender` (`MediatR`). Each action body changes from `await x.SomeAsync(User.GetUserId(), ...)` to `await sender.Send(new SomeCommand(User.GetUserId(), ...), ct)`. Routes, HTTP verbs, status codes, and response types are unchanged. `RolesController` and `MenuController` keep their existing `IPermissionService` dependency alongside the new `ISender`. ## DI wiring `mws.infrastructure/DependencyInjection.cs`: - Remove the 8 `services.AddScoped()` lines for the 7 converted modules (Auth, Accounts, Roles, Projects, ProjectMembers, Documents, Tasks). - Add `services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Mws.Application.Common.IUnitOfWork).Assembly));` - Keep `IPermissionService`, `ITokenService`, `IPasswordHasher` registrations unchanged. ## Error handling Unchanged. Handlers throw the same `Mws.Application.Common.Exceptions` types the services did; `ApiExceptionMiddleware` still catches them at the API boundary — nothing about that pipeline changes. ## Testing / verification No test project exists in this repo (per `CLAUDE.md`, none expected today). Verification is: 1. `dotnet build Mws.slnx` succeeds with no errors. 2. `dotnet run --project Mws.Api`, then smoke-test with curl: login (Auth), list + create a project (Projects, command + query path), get menu (confirms `IPermissionService` still wired and untouched). ## Out of scope - AutoMapper stays as-is. - No FluentValidation / MediatR pipeline behaviors — not requested, and the ladder says don't add abstractions nobody asked for. Can be layered on later since the request/handler shape is already in place. - No repository/read-model split — queries still go through the existing `IUnitOfWork` repositories.