- name
- moai-cqrs-backend
- description
- Backend CQRS three-layer code standards for MoAI (.NET 9, Maomi modules, MediatR, EF Core). Use when adding/modifying Commands, Queries, Handlers, Controllers, validators, or business rules in src/*. 仅限 MoAI 项目。Use only for the MoAI project.
# MoAI 后端 CQRS 三层规范(L2)
## PROJECT SCOPE
只服务 MoAI 后端 `src/`。**REQUIRED REFERENCE:改代码前必读 [docs/cqrs-conventions.md](../../../docs/cqrs-conventions.md)(唯一真源,含完整代码模板与命名规范)。** 本 skill 只写执行要点与实踩坑。
## WHEN
- 新增/修改 Command、Query、Handler、Controller、校验
- 涉及 `src/<module>/MoAI.<Module>.{Shared,Core,Api}` 任一层的改动
## WHAT
按三层依赖(`Api → Core → Shared`)产出合规范的 CQRS 代码,构建 0 错。
## HOW
### 0. 先找范本
`rg` 找同域代码读真实实现。最佳完整范本:**user-management**(`src/account/` 的 UpdateUserIsAdminCommand 全链路)。
### 1. 目录与命名(细则见真源 §目录结构/命名规范)
| 项 | 规范 |
|---|---|
| Command | `{动作}{实体}Command.cs`,命名空间 `MoAI.{Domain}.Commands` |
| Query | `Query{实体}{描述}Command.cs`,命名空间 `MoAI.{Domain}.Queries` |
| Handler | `{Command/Query名}Handler.cs`;Command 在 `Handlers/`,Query 在 `Queries/` |
| Response | `{Query名}Response.cs` / `{Query名}ResponseItem.cs`,在 `Queries/Responses/` |
| 复杂模块 | 可按子领域建子目录(参照 `MoAI.Plugin.Shared/Classify/...`) |
### 2. Shared 层要点
- Command/Query 都继承 `IModelValidator<T>`(FluentValidation 静态 `Validate`),提前拦截无效请求
- ⚠️ **路由参数时序坑**:`{id}` 由 Controller 回填,自动验证发生在回填之前——Validate 里只能校验请求体字段,校验路由回填字段 = 接口恒 400(oauthconnect PUT 实踩)
- 需要用户上下文的命令继承 `IUserIdContext`(`ContextUserId`/`ContextUserType`);用不到就别继承
- ⚠️ `IUserIdContext` 的两个属性**必须加 `[JsonIgnore]`**:否则会作为查询参数/请求体字段进入 OpenAPI,进而出现在 Kiota 生成的客户端里(查询型命令尤其明显)。仓库先例:`PagedParamter` 对内部属性同样加 `[JsonIgnore]`。Handler 侧读 `request.ContextUserId`,不要注入 `IUserContextProvider`
- 分页继承 `PagedParamter`(上限 1000);写命令响应统一 `EmptyCommandResponse`
- 公开成员全部中文 XML 注释(StyleCop 强制)
### 3. Core 层要点
- Handler 构造注入 `DatabaseContext` + 领域服务;**禁止注入 IUserContextProvider/UserContext**(用户信息只能经 Command 的 IUserIdContext 传入)
- 实体审计属性(CreateUserId/CreateTime/UpdateUserId/UpdateTime/IsDeleted)框架自动注入,**不要手动赋值**;软删除字段 `IsDeleted` 类型是 **long**(0=未删),查询记得 `IsDeleted == 0`,勿写成 bool
- 目标保护依赖 DB 事实:root 判定 = `setting` 表 `key="root"` 的 value
- 业务异常:`throw new BusinessException("中文消息.") { StatusCode = 400/403/404/409 }`,禁止裸 500
- ⚠️ **写用户相关数据后必须 `RemoveUserStateAsync`** 失效 Redis 用户态(禁用/降权即时生效依赖此)
### 4. Api 层要点
- Controller 只做门禁 + 转发,无业务逻辑
- 角色门禁在 Controller(`EnsureAdminAsync`/`EnsureRootAsync` 私有方法,查 `GetUserStateAsync`,非 403 即抛);目标保护在 Handler
- 路由参数显式回填 + `_userContextProvider.SetUserContext(cmd)` 后再 `_mediator.Send`
- 用户上下文只经 `IUserContextProvider.GetUserContext()`,不直接注入 UserContext
### 5. 模块注册
三层各有 `{Domain}SharedModule`/`{Domain}CoreModule`/`{Domain}ApiModule`(`IModule`,Core 用 `[InjectModule<...>]` 声明依赖,模板见真源 §模块注册)。
## REFERENCE
正例:`UpdateUserIsAdminCommand` 全链路(Shared 定义 → Handler 目标保护+缓存失效 → Controller EnsureRoot+回填)。
## LIMITS
- 不含前端规范(`L2-code-standards/moai-frontend-ui`);审查清单(`L3-fix-standards/moai-cqrs-review`)
- 不做迁移/DDL、部署
عرض على GitHub