@atlashub/smartstack-cli 3.39.0 → 3.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. package/.documentation/apex.html +644 -644
  2. package/.documentation/css/styles.css +2320 -2320
  3. package/.documentation/init.html +1377 -1377
  4. package/.documentation/js/app.js +780 -780
  5. package/.documentation/prd-json-v2.0.0.md +396 -396
  6. package/.documentation/testing-ba-e2e.md +462 -462
  7. package/config/default-config.json +95 -95
  8. package/config/mcp-defaults.json +62 -62
  9. package/config/settings.json +53 -53
  10. package/config/settings.local.example.json +16 -16
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp-entry.mjs +6 -4
  13. package/dist/mcp-entry.mjs.map +1 -1
  14. package/package.json +115 -115
  15. package/scripts/extract-api-endpoints.ts +325 -325
  16. package/scripts/extract-business-rules.ts +440 -440
  17. package/scripts/generate-doc-with-mock-ui.ts +804 -804
  18. package/scripts/health-check.sh +168 -168
  19. package/scripts/postinstall.js +18 -18
  20. package/templates/agents/action.md +37 -37
  21. package/templates/agents/ba-reader.md +378 -378
  22. package/templates/agents/ba-writer.md +861 -861
  23. package/templates/agents/code-reviewer.md +163 -163
  24. package/templates/agents/db-reader.md +149 -149
  25. package/templates/agents/docs-context-reader.md +143 -143
  26. package/templates/agents/docs-sync-checker.md +122 -122
  27. package/templates/agents/efcore/conflicts.md +84 -84
  28. package/templates/agents/efcore/db-deploy.md +74 -74
  29. package/templates/agents/efcore/db-reset.md +85 -85
  30. package/templates/agents/efcore/db-seed.md +61 -61
  31. package/templates/agents/efcore/db-status.md +86 -86
  32. package/templates/agents/efcore/migration.md +186 -186
  33. package/templates/agents/efcore/rebase-snapshot.md +108 -108
  34. package/templates/agents/efcore/scan.md +92 -92
  35. package/templates/agents/efcore/squash.md +161 -161
  36. package/templates/agents/explore-codebase.md +66 -66
  37. package/templates/agents/explore-docs.md +98 -98
  38. package/templates/agents/fix-grammar.md +50 -50
  39. package/templates/agents/gitflow/abort.md +45 -45
  40. package/templates/agents/gitflow/cleanup.md +96 -96
  41. package/templates/agents/gitflow/commit.md +236 -236
  42. package/templates/agents/gitflow/exec.md +48 -48
  43. package/templates/agents/gitflow/finish.md +146 -146
  44. package/templates/agents/gitflow/init-clone.md +199 -199
  45. package/templates/agents/gitflow/init-detect.md +137 -137
  46. package/templates/agents/gitflow/init-validate.md +225 -225
  47. package/templates/agents/gitflow/init.md +340 -340
  48. package/templates/agents/gitflow/merge.md +145 -145
  49. package/templates/agents/gitflow/plan.md +42 -42
  50. package/templates/agents/gitflow/pr.md +191 -191
  51. package/templates/agents/gitflow/review.md +49 -49
  52. package/templates/agents/gitflow/start.md +147 -147
  53. package/templates/agents/gitflow/status.md +95 -95
  54. package/templates/agents/mcp-healthcheck.md +163 -163
  55. package/templates/agents/snipper.md +37 -37
  56. package/templates/agents/websearch.md +46 -46
  57. package/templates/hooks/appsettings-guard.sh +76 -76
  58. package/templates/hooks/docs-drift-check.md +96 -96
  59. package/templates/hooks/ef-migration-check.md +139 -139
  60. package/templates/hooks/hooks.json +58 -58
  61. package/templates/hooks/mcp-check.md +64 -64
  62. package/templates/hooks/ralph-mcp-logger.sh +46 -46
  63. package/templates/hooks/ralph-session-end.sh +69 -69
  64. package/templates/hooks/stop-hook.sh +177 -177
  65. package/templates/hooks/wsl-dotnet-cleanup.sh +24 -24
  66. package/templates/mcp-scaffolding/component.tsx.hbs +318 -318
  67. package/templates/mcp-scaffolding/controller.cs.hbs +192 -192
  68. package/templates/mcp-scaffolding/entity-extension.cs.hbs +239 -239
  69. package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +116 -116
  70. package/templates/mcp-scaffolding/frontend/nav-routes.ts.hbs +133 -133
  71. package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +126 -126
  72. package/templates/mcp-scaffolding/migrations/seed-roles.cs.hbs +261 -261
  73. package/templates/mcp-scaffolding/service-extension.cs.hbs +53 -53
  74. package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +436 -436
  75. package/templates/mcp-scaffolding/tests/entity.test.cs.hbs +239 -239
  76. package/templates/mcp-scaffolding/tests/repository.test.cs.hbs +441 -441
  77. package/templates/mcp-scaffolding/tests/security.test.cs.hbs +442 -442
  78. package/templates/mcp-scaffolding/tests/service.test.cs.hbs +402 -402
  79. package/templates/mcp-scaffolding/tests/validator.test.cs.hbs +428 -428
  80. package/templates/project/DependencyInjection.Application.cs.template +25 -25
  81. package/templates/project/DependencyInjection.Infrastructure.cs.template +61 -61
  82. package/templates/project/DesignTimeExtensionsDbContextFactory.cs.template +70 -70
  83. package/templates/project/ExampleEntity.cs.template +116 -116
  84. package/templates/project/ExampleEntityConfiguration.cs.template +64 -64
  85. package/templates/project/ExampleService.cs.template +146 -146
  86. package/templates/project/ExtensionsDbContext.cs.template +41 -41
  87. package/templates/project/IExtensionsDbContext.cs.template +22 -22
  88. package/templates/project/Program.cs.template +47 -47
  89. package/templates/project/README.md +79 -79
  90. package/templates/project/api.ts.template +12 -12
  91. package/templates/project/appsettings.json.template +170 -170
  92. package/templates/project/claude-settings.json.template +5 -5
  93. package/templates/project/test-frontend/msw/handlers.ts +58 -58
  94. package/templates/project/test-frontend/msw/server.ts +25 -25
  95. package/templates/project/test-frontend/setup.ts +16 -16
  96. package/templates/project/test-frontend/test-utils.tsx +59 -59
  97. package/templates/project/test-frontend/vitest.config.ts +31 -31
  98. package/templates/ralph/README.md +93 -93
  99. package/templates/ralph/ralph.config.yaml +113 -113
  100. package/templates/scripts/setup-ralph-loop.sh +173 -173
  101. package/templates/skills/_resources/config-safety.md +61 -61
  102. package/templates/skills/_resources/context-digest-template.md +53 -53
  103. package/templates/skills/_resources/doc-context-cache.md +60 -60
  104. package/templates/skills/_resources/docs-manifest-schema.md +155 -155
  105. package/templates/skills/_resources/formatting-guide.md +124 -124
  106. package/templates/skills/_resources/mcp-validate-documentation-spec.md +181 -181
  107. package/templates/skills/_shared.md +228 -228
  108. package/templates/skills/admin/SKILL.md +48 -48
  109. package/templates/skills/ai-prompt/SKILL.md +107 -107
  110. package/templates/skills/ai-prompt/steps/step-00-init.md +47 -47
  111. package/templates/skills/ai-prompt/steps/step-01-implementation.md +122 -122
  112. package/templates/skills/apex/SKILL.md +168 -168
  113. package/templates/skills/apex/_shared.md +141 -141
  114. package/templates/skills/apex/references/agent-teams-protocol.md +164 -164
  115. package/templates/skills/apex/references/analysis-methods.md +141 -141
  116. package/templates/skills/apex/references/challenge-questions.md +145 -145
  117. package/templates/skills/apex/references/code-generation.md +412 -412
  118. package/templates/skills/apex/references/core-seed-data.md +1437 -1437
  119. package/templates/skills/apex/references/error-classification.md +144 -144
  120. package/templates/skills/apex/references/examine-build-validation.md +82 -82
  121. package/templates/skills/apex/references/execution-frontend-gates.md +177 -177
  122. package/templates/skills/apex/references/execution-frontend-patterns.md +105 -105
  123. package/templates/skills/apex/references/execution-layer1-rules.md +96 -96
  124. package/templates/skills/apex/references/initialization-challenge-flow.md +110 -110
  125. package/templates/skills/apex/references/planning-layer-mapping.md +151 -151
  126. package/templates/skills/apex/references/post-checks.md +1584 -1584
  127. package/templates/skills/apex/references/smartstack-api.md +1053 -1053
  128. package/templates/skills/apex/references/smartstack-frontend.md +1571 -1571
  129. package/templates/skills/apex/references/smartstack-layers.md +402 -402
  130. package/templates/skills/apex/steps/step-00-init.md +307 -307
  131. package/templates/skills/apex/steps/step-01-analyze.md +165 -165
  132. package/templates/skills/apex/steps/step-02-plan.md +144 -144
  133. package/templates/skills/apex/steps/step-03-execute.md +328 -328
  134. package/templates/skills/apex/steps/step-04-examine.md +263 -263
  135. package/templates/skills/apex/steps/step-05-deep-review.md +129 -129
  136. package/templates/skills/apex/steps/step-06-resolve.md +101 -101
  137. package/templates/skills/apex/steps/step-07-tests.md +238 -238
  138. package/templates/skills/apex/steps/step-08-run-tests.md +125 -125
  139. package/templates/skills/application/SKILL.md +4 -4
  140. package/templates/skills/application/references/application-roles-template.md +227 -227
  141. package/templates/skills/application/references/backend-controller-hierarchy.md +58 -58
  142. package/templates/skills/application/references/backend-entity-seeding.md +72 -72
  143. package/templates/skills/application/references/backend-seeding-and-dto-output.md +83 -83
  144. package/templates/skills/application/references/backend-table-prefix-mapping.md +79 -79
  145. package/templates/skills/application/references/backend-verification.md +88 -88
  146. package/templates/skills/application/references/frontend-i18n-and-output.md +67 -67
  147. package/templates/skills/application/references/frontend-route-naming.md +117 -117
  148. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +107 -107
  149. package/templates/skills/application/references/frontend-verification.md +156 -156
  150. package/templates/skills/application/references/migration-checklist-troubleshooting.md +1 -1
  151. package/templates/skills/application/references/provider-template.md +177 -177
  152. package/templates/skills/application/references/roles-client-project-handling.md +55 -55
  153. package/templates/skills/application/references/roles-fallback-procedure.md +149 -149
  154. package/templates/skills/application/references/test-coverage-requirements.md +213 -213
  155. package/templates/skills/application/references/test-frontend.md +73 -73
  156. package/templates/skills/application/references/test-prerequisites.md +72 -72
  157. package/templates/skills/application/steps/step-05-frontend.md +176 -176
  158. package/templates/skills/application/steps/step-06-migration.md +193 -193
  159. package/templates/skills/application/steps/step-07-tests.md +356 -356
  160. package/templates/skills/application/steps/step-08-documentation.md +137 -137
  161. package/templates/skills/application/templates-backend.md +463 -463
  162. package/templates/skills/application/templates-frontend.md +685 -685
  163. package/templates/skills/application/templates-i18n.md +520 -520
  164. package/templates/skills/application/templates-seed.md +1096 -1096
  165. package/templates/skills/business-analyse/SKILL.md +327 -327
  166. package/templates/skills/business-analyse/_architecture.md +123 -123
  167. package/templates/skills/business-analyse/_elicitation.md +206 -206
  168. package/templates/skills/business-analyse/_module-loop.md +115 -115
  169. package/templates/skills/business-analyse/_shared.md +383 -383
  170. package/templates/skills/business-analyse/_suggestions.md +34 -34
  171. package/templates/skills/business-analyse/html/ba-interactive.html +4477 -4477
  172. package/templates/skills/business-analyse/html/build-html.js +77 -77
  173. package/templates/skills/business-analyse/html/src/scripts/01-data-init.js +150 -150
  174. package/templates/skills/business-analyse/html/src/scripts/02-navigation.js +227 -227
  175. package/templates/skills/business-analyse/html/src/scripts/03-render-cadrage.js +199 -199
  176. package/templates/skills/business-analyse/html/src/scripts/04-render-modules.js +205 -205
  177. package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +647 -647
  178. package/templates/skills/business-analyse/html/src/scripts/06-render-consolidation.js +195 -195
  179. package/templates/skills/business-analyse/html/src/scripts/07-render-handoff.js +92 -92
  180. package/templates/skills/business-analyse/html/src/scripts/08-editing.js +135 -135
  181. package/templates/skills/business-analyse/html/src/scripts/09-export.js +168 -168
  182. package/templates/skills/business-analyse/html/src/scripts/10-comments.js +171 -171
  183. package/templates/skills/business-analyse/html/src/scripts/11-review-panel.js +166 -166
  184. package/templates/skills/business-analyse/html/src/styles/01-variables.css +38 -38
  185. package/templates/skills/business-analyse/html/src/styles/02-layout.css +101 -101
  186. package/templates/skills/business-analyse/html/src/styles/03-navigation.css +120 -120
  187. package/templates/skills/business-analyse/html/src/styles/04-cards.css +196 -196
  188. package/templates/skills/business-analyse/html/src/styles/05-modules.css +454 -454
  189. package/templates/skills/business-analyse/html/src/styles/06-wireframes.css +272 -272
  190. package/templates/skills/business-analyse/html/src/styles/07-comments.css +184 -184
  191. package/templates/skills/business-analyse/html/src/styles/08-review-panel.css +241 -241
  192. package/templates/skills/business-analyse/html/src/template.html +516 -516
  193. package/templates/skills/business-analyse/patterns/suggestion-catalog.md +546 -546
  194. package/templates/skills/business-analyse/questionnaire/00-application.md +160 -160
  195. package/templates/skills/business-analyse/questionnaire/00b-project.md +85 -85
  196. package/templates/skills/business-analyse/questionnaire/01-context.md +185 -185
  197. package/templates/skills/business-analyse/questionnaire/02-stakeholders.md +189 -189
  198. package/templates/skills/business-analyse/questionnaire/03-scope.md +164 -164
  199. package/templates/skills/business-analyse/questionnaire/04-data.md +88 -88
  200. package/templates/skills/business-analyse/questionnaire/05-integrations.md +58 -58
  201. package/templates/skills/business-analyse/questionnaire/06-security.md +68 -68
  202. package/templates/skills/business-analyse/questionnaire/07-ui.md +76 -76
  203. package/templates/skills/business-analyse/questionnaire/08-performance.md +42 -42
  204. package/templates/skills/business-analyse/questionnaire/09-constraints.md +45 -45
  205. package/templates/skills/business-analyse/questionnaire/10-documentation.md +43 -43
  206. package/templates/skills/business-analyse/questionnaire/11-data-lifecycle.md +59 -59
  207. package/templates/skills/business-analyse/questionnaire/12-migration.md +58 -58
  208. package/templates/skills/business-analyse/questionnaire/13-cross-module.md +69 -69
  209. package/templates/skills/business-analyse/questionnaire/14-risk-assumptions.md +135 -135
  210. package/templates/skills/business-analyse/questionnaire/15-success-metrics.md +136 -136
  211. package/templates/skills/business-analyse/questionnaire.md +337 -337
  212. package/templates/skills/business-analyse/react/application-viewer.md +242 -242
  213. package/templates/skills/business-analyse/react/components.md +551 -551
  214. package/templates/skills/business-analyse/react/i18n-template.md +306 -306
  215. package/templates/skills/business-analyse/references/acceptance-criteria.md +169 -169
  216. package/templates/skills/business-analyse/references/agent-module-prompt.md +362 -362
  217. package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +557 -557
  218. package/templates/skills/business-analyse/references/analysis-semantic-checks.md +190 -190
  219. package/templates/skills/business-analyse/references/cache-warming-strategy.md +566 -566
  220. package/templates/skills/business-analyse/references/cadrage-challenge-patterns.md +41 -41
  221. package/templates/skills/business-analyse/references/cadrage-coverage-matrix.md +74 -74
  222. package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +115 -115
  223. package/templates/skills/business-analyse/references/cadrage-shared-modules.md +68 -69
  224. package/templates/skills/business-analyse/references/cadrage-structure-cards.md +85 -85
  225. package/templates/skills/business-analyse/references/compilation-structure-cards.md +297 -297
  226. package/templates/skills/business-analyse/references/consolidation-structural-checks.md +107 -107
  227. package/templates/skills/business-analyse/references/deploy-data-build.md +180 -180
  228. package/templates/skills/business-analyse/references/deploy-modes.md +118 -118
  229. package/templates/skills/business-analyse/references/detection-strategies.md +424 -424
  230. package/templates/skills/business-analyse/references/entity-architecture-decision.md +218 -218
  231. package/templates/skills/business-analyse/references/handoff-file-templates.md +120 -120
  232. package/templates/skills/business-analyse/references/handoff-mappings.md +81 -81
  233. package/templates/skills/business-analyse/references/handoff-seeddata-generation.md +312 -312
  234. package/templates/skills/business-analyse/references/html-data-mapping.md +299 -299
  235. package/templates/skills/business-analyse/references/init-schema-deployment.md +65 -65
  236. package/templates/skills/business-analyse/references/naming-conventions.md +243 -243
  237. package/templates/skills/business-analyse/references/prd-generation.md +258 -258
  238. package/templates/skills/business-analyse/references/review-data-mapping.md +363 -363
  239. package/templates/skills/business-analyse/references/robustness-checks.md +542 -542
  240. package/templates/skills/business-analyse/references/spec-auto-inference.md +111 -111
  241. package/templates/skills/business-analyse/references/team-orchestration.md +1022 -1022
  242. package/templates/skills/business-analyse/references/ui-dashboard-spec.md +85 -85
  243. package/templates/skills/business-analyse/references/ui-resource-cards.md +259 -259
  244. package/templates/skills/business-analyse/references/validate-incremental-html.md +121 -121
  245. package/templates/skills/business-analyse/references/validation-checklist.md +347 -347
  246. package/templates/skills/business-analyse/references/wireframe-svg-style-guide.md +335 -335
  247. package/templates/skills/business-analyse/schemas/application-schema.json +453 -453
  248. package/templates/skills/business-analyse/schemas/feature-schema.json +53 -53
  249. package/templates/skills/business-analyse/schemas/project-schema.json +485 -485
  250. package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +201 -201
  251. package/templates/skills/business-analyse/schemas/sections/discovery-schema.json +82 -82
  252. package/templates/skills/business-analyse/schemas/sections/handoff-schema.json +80 -80
  253. package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +70 -70
  254. package/templates/skills/business-analyse/schemas/sections/specification-schema.json +547 -547
  255. package/templates/skills/business-analyse/schemas/sections/validation-schema.json +93 -93
  256. package/templates/skills/business-analyse/schemas/shared/common-defs.json +226 -226
  257. package/templates/skills/business-analyse/steps/step-00-init.md +575 -576
  258. package/templates/skills/business-analyse/steps/step-01-cadrage.md +767 -767
  259. package/templates/skills/business-analyse/steps/step-01b-applications.md +419 -419
  260. package/templates/skills/business-analyse/steps/step-02-decomposition.md +387 -387
  261. package/templates/skills/business-analyse/steps/step-03a-data.md +16 -16
  262. package/templates/skills/business-analyse/steps/step-03a1-setup.md +506 -506
  263. package/templates/skills/business-analyse/steps/step-03a2-analysis.md +252 -252
  264. package/templates/skills/business-analyse/steps/step-03b-ui.md +425 -425
  265. package/templates/skills/business-analyse/steps/step-03c-compile.md +611 -611
  266. package/templates/skills/business-analyse/steps/step-03d-validate.md +783 -783
  267. package/templates/skills/business-analyse/steps/step-04-consolidation.md +17 -17
  268. package/templates/skills/business-analyse/steps/step-04a-collect.md +415 -415
  269. package/templates/skills/business-analyse/steps/step-04b-analyze.md +163 -163
  270. package/templates/skills/business-analyse/steps/step-04c-decide.md +186 -186
  271. package/templates/skills/business-analyse/steps/step-05a-handoff.md +840 -840
  272. package/templates/skills/business-analyse/steps/step-05b-deploy.md +522 -522
  273. package/templates/skills/business-analyse/steps/step-05c-ralph-readiness.md +703 -703
  274. package/templates/skills/business-analyse/steps/step-06-review.md +278 -278
  275. package/templates/skills/business-analyse/templates/tpl-frd.md +168 -168
  276. package/templates/skills/business-analyse/templates/tpl-handoff.md +186 -186
  277. package/templates/skills/business-analyse/templates/tpl-launch-displays.md +59 -59
  278. package/templates/skills/business-analyse/templates/tpl-progress.md +172 -172
  279. package/templates/skills/business-analyse/templates-frd.md +476 -476
  280. package/templates/skills/business-analyse/templates-react.md +574 -574
  281. package/templates/skills/cc-agent/SKILL.md +129 -129
  282. package/templates/skills/cc-agent/references/agent-behavior-patterns.md +95 -95
  283. package/templates/skills/cc-agent/references/agent-frontmatter.md +213 -213
  284. package/templates/skills/cc-agent/references/permission-modes.md +102 -102
  285. package/templates/skills/cc-agent/references/tools-reference.md +144 -144
  286. package/templates/skills/cc-agent/steps/step-00-init.md +134 -134
  287. package/templates/skills/cc-agent/steps/step-01-design.md +186 -186
  288. package/templates/skills/cc-agent/steps/step-02-generate.md +131 -131
  289. package/templates/skills/cc-agent/steps/step-03-validate.md +130 -130
  290. package/templates/skills/cc-agent/templates/agent-categorized.md +67 -67
  291. package/templates/skills/cc-agent/templates/agent-standalone.md +56 -56
  292. package/templates/skills/cc-agent/templates/agent-with-skills.md +94 -94
  293. package/templates/skills/cc-audit/SKILL.md +108 -108
  294. package/templates/skills/cc-audit/references/agent-checklist.md +91 -91
  295. package/templates/skills/cc-audit/references/hook-checklist.md +110 -110
  296. package/templates/skills/cc-audit/references/skill-checklist.md +70 -70
  297. package/templates/skills/cc-audit/steps/step-00-init.md +98 -98
  298. package/templates/skills/cc-audit/steps/step-01-scan.md +142 -142
  299. package/templates/skills/cc-audit/steps/step-02-analyze.md +158 -158
  300. package/templates/skills/cc-audit/steps/step-03-report.md +142 -142
  301. package/templates/skills/cc-skill/SKILL.md +134 -134
  302. package/templates/skills/cc-skill/references/best-practices.md +167 -167
  303. package/templates/skills/cc-skill/references/frontmatter-reference.md +182 -182
  304. package/templates/skills/cc-skill/references/skill-patterns.md +199 -199
  305. package/templates/skills/cc-skill/steps/step-00-init.md +119 -119
  306. package/templates/skills/cc-skill/steps/step-01-design.md +199 -199
  307. package/templates/skills/cc-skill/steps/step-02-generate.md +145 -145
  308. package/templates/skills/cc-skill/steps/step-03-steps.md +151 -151
  309. package/templates/skills/cc-skill/steps/step-04-validate.md +124 -124
  310. package/templates/skills/cc-skill/templates/skill-forked.md +85 -85
  311. package/templates/skills/cc-skill/templates/skill-progressive.md +102 -102
  312. package/templates/skills/cc-skill/templates/skill-simple.md +75 -75
  313. package/templates/skills/cc-skill/templates/step-template.md +82 -82
  314. package/templates/skills/check-version/SKILL.md +196 -196
  315. package/templates/skills/controller/SKILL.md +162 -162
  316. package/templates/skills/controller/postman-templates.md +614 -614
  317. package/templates/skills/controller/references/controller-code-templates.md +159 -159
  318. package/templates/skills/controller/references/mcp-scaffold-workflow.md +209 -209
  319. package/templates/skills/controller/references/permission-sync-templates.md +149 -149
  320. package/templates/skills/controller/steps/step-00-init.md +193 -191
  321. package/templates/skills/controller/steps/step-01-analyze.md +146 -146
  322. package/templates/skills/controller/steps/step-02-plan.md +176 -176
  323. package/templates/skills/controller/steps/step-03-generate.md +189 -189
  324. package/templates/skills/controller/steps/step-04-perms.md +80 -80
  325. package/templates/skills/controller/steps/step-05-validate.md +107 -107
  326. package/templates/skills/controller/templates.md +1555 -1555
  327. package/templates/skills/debug/SKILL.md +70 -70
  328. package/templates/skills/debug/references/team-protocol.md +232 -232
  329. package/templates/skills/debug/steps/step-00-init.md +57 -57
  330. package/templates/skills/debug/steps/step-01-analyze.md +219 -219
  331. package/templates/skills/debug/steps/step-02-resolve.md +85 -85
  332. package/templates/skills/documentation/SKILL.md +132 -132
  333. package/templates/skills/documentation/data-schema.md +227 -227
  334. package/templates/skills/documentation/steps/step-00-init.md +70 -70
  335. package/templates/skills/documentation/steps/step-01-scan.md +113 -113
  336. package/templates/skills/documentation/steps/step-02-generate.md +231 -231
  337. package/templates/skills/documentation/steps/step-03-validate.md +251 -238
  338. package/templates/skills/documentation/templates.md +662 -663
  339. package/templates/skills/efcore/SKILL.md +167 -167
  340. package/templates/skills/efcore/references/both-contexts.md +32 -32
  341. package/templates/skills/efcore/references/database-operations.md +67 -67
  342. package/templates/skills/efcore/references/destructive-operations.md +38 -38
  343. package/templates/skills/efcore/references/reset-operations.md +81 -81
  344. package/templates/skills/efcore/references/seed-methods.md +86 -86
  345. package/templates/skills/efcore/references/shared-init-functions.md +250 -250
  346. package/templates/skills/efcore/references/sql-objects-injection.md +61 -61
  347. package/templates/skills/efcore/references/troubleshooting.md +81 -81
  348. package/templates/skills/efcore/references/zero-downtime-patterns.md +227 -227
  349. package/templates/skills/efcore/steps/db/step-deploy.md +217 -217
  350. package/templates/skills/efcore/steps/db/step-reset.md +186 -186
  351. package/templates/skills/efcore/steps/db/step-seed.md +166 -166
  352. package/templates/skills/efcore/steps/db/step-status.md +173 -173
  353. package/templates/skills/efcore/steps/migration/step-00-init.md +102 -102
  354. package/templates/skills/efcore/steps/migration/step-01-check.md +164 -164
  355. package/templates/skills/efcore/steps/migration/step-02-create.md +160 -160
  356. package/templates/skills/efcore/steps/migration/step-03-validate.md +168 -168
  357. package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +173 -173
  358. package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +100 -100
  359. package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +115 -115
  360. package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +112 -112
  361. package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +157 -157
  362. package/templates/skills/efcore/steps/shared/step-00-init.md +131 -131
  363. package/templates/skills/efcore/steps/squash/step-00-init.md +141 -141
  364. package/templates/skills/efcore/steps/squash/step-01-backup.md +120 -120
  365. package/templates/skills/efcore/steps/squash/step-02-fetch.md +168 -168
  366. package/templates/skills/efcore/steps/squash/step-03-create.md +184 -184
  367. package/templates/skills/efcore/steps/squash/step-04-validate.md +174 -174
  368. package/templates/skills/explore/SKILL.md +98 -98
  369. package/templates/skills/feature-full/SKILL.md +111 -111
  370. package/templates/skills/feature-full/steps/step-00-init.md +57 -57
  371. package/templates/skills/feature-full/steps/step-01-implementation.md +120 -120
  372. package/templates/skills/gitflow/SKILL.md +377 -377
  373. package/templates/skills/gitflow/_shared.md +620 -620
  374. package/templates/skills/gitflow/phases/abort.md +189 -189
  375. package/templates/skills/gitflow/phases/cleanup.md +234 -234
  376. package/templates/skills/gitflow/phases/status.md +192 -192
  377. package/templates/skills/gitflow/references/commit-message-generation.md +58 -58
  378. package/templates/skills/gitflow/references/commit-migration-validation.md +49 -49
  379. package/templates/skills/gitflow/references/finish-cleanup.md +55 -55
  380. package/templates/skills/gitflow/references/finish-version-bumping.md +45 -45
  381. package/templates/skills/gitflow/references/init-config-template.md +135 -135
  382. package/templates/skills/gitflow/references/init-environment-detection.md +41 -41
  383. package/templates/skills/gitflow/references/init-name-normalization.md +103 -103
  384. package/templates/skills/gitflow/references/init-questions.md +185 -185
  385. package/templates/skills/gitflow/references/init-structure-creation.md +75 -75
  386. package/templates/skills/gitflow/references/init-version-detection.md +21 -21
  387. package/templates/skills/gitflow/references/init-workspace-detection.md +43 -43
  388. package/templates/skills/gitflow/references/merge-ci-status.md +36 -36
  389. package/templates/skills/gitflow/references/merge-execution.md +62 -62
  390. package/templates/skills/gitflow/references/merge-pr-context.md +76 -76
  391. package/templates/skills/gitflow/references/plan-template.md +69 -69
  392. package/templates/skills/gitflow/references/pr-build-checks.md +60 -60
  393. package/templates/skills/gitflow/references/pr-generation.md +58 -58
  394. package/templates/skills/gitflow/references/start-branch-normalization.md +28 -28
  395. package/templates/skills/gitflow/references/start-efcore-preflight.md +70 -70
  396. package/templates/skills/gitflow/references/start-local-config.md +113 -113
  397. package/templates/skills/gitflow/references/start-worktree-creation.md +50 -50
  398. package/templates/skills/gitflow/references/sync-push-verify.md +44 -44
  399. package/templates/skills/gitflow/references/sync-rebase-conflicts.md +38 -38
  400. package/templates/skills/gitflow/steps/step-commit.md +199 -199
  401. package/templates/skills/gitflow/steps/step-finish.md +147 -147
  402. package/templates/skills/gitflow/steps/step-init.md +190 -190
  403. package/templates/skills/gitflow/steps/step-merge.md +85 -85
  404. package/templates/skills/gitflow/steps/step-plan.md +151 -151
  405. package/templates/skills/gitflow/steps/step-pr.md +199 -199
  406. package/templates/skills/gitflow/steps/step-start.md +195 -195
  407. package/templates/skills/gitflow/steps/step-sync.md +161 -161
  408. package/templates/skills/gitflow/templates/config.json +72 -72
  409. package/templates/skills/mcp/SKILL.md +62 -62
  410. package/templates/skills/mcp/steps/step-01-healthcheck.md +108 -108
  411. package/templates/skills/mcp/steps/step-02-tools.md +73 -73
  412. package/templates/skills/notification/SKILL.md +173 -173
  413. package/templates/skills/quick-search/SKILL.md +99 -99
  414. package/templates/skills/ralph-loop/SKILL.md +234 -234
  415. package/templates/skills/ralph-loop/references/category-completeness.md +185 -185
  416. package/templates/skills/ralph-loop/references/category-rules.md +96 -96
  417. package/templates/skills/ralph-loop/references/compact-loop.md +300 -300
  418. package/templates/skills/ralph-loop/references/init-resume-recovery.md +127 -127
  419. package/templates/skills/ralph-loop/references/module-transition.md +151 -151
  420. package/templates/skills/ralph-loop/references/multi-module-queue.md +171 -171
  421. package/templates/skills/ralph-loop/references/parallel-execution.md +246 -246
  422. package/templates/skills/ralph-loop/references/section-splitting.md +439 -439
  423. package/templates/skills/ralph-loop/references/task-transform-legacy.md +256 -256
  424. package/templates/skills/ralph-loop/references/team-orchestration.md +547 -547
  425. package/templates/skills/ralph-loop/steps/step-00-init.md +150 -150
  426. package/templates/skills/ralph-loop/steps/step-01-task.md +174 -174
  427. package/templates/skills/ralph-loop/steps/step-02-execute.md +177 -177
  428. package/templates/skills/ralph-loop/steps/step-03-commit.md +92 -92
  429. package/templates/skills/ralph-loop/steps/step-04-check.md +207 -207
  430. package/templates/skills/ralph-loop/steps/step-05-report.md +175 -175
  431. package/templates/skills/refactor/SKILL.md +56 -56
  432. package/templates/skills/refactor/steps/step-01-discover.md +60 -60
  433. package/templates/skills/refactor/steps/step-02-execute.md +67 -67
  434. package/templates/skills/review-code/SKILL.md +94 -94
  435. package/templates/skills/review-code/references/clean-code-principles.md +292 -292
  436. package/templates/skills/review-code/references/code-quality-metrics.md +174 -174
  437. package/templates/skills/review-code/references/feedback-patterns.md +149 -149
  438. package/templates/skills/review-code/references/owasp-api-top10.md +243 -243
  439. package/templates/skills/review-code/references/security-checklist.md +212 -212
  440. package/templates/skills/review-code/steps/step-01-smartstack.md +96 -96
  441. package/templates/skills/review-code/steps/step-02-detailed-review.md +80 -80
  442. package/templates/skills/review-code/steps/step-03-react.md +44 -44
  443. package/templates/skills/ui-components/SKILL.md +137 -137
  444. package/templates/skills/ui-components/accessibility.md +170 -170
  445. package/templates/skills/ui-components/patterns/dashboard-chart.md +327 -327
  446. package/templates/skills/ui-components/patterns/data-table.md +39 -39
  447. package/templates/skills/ui-components/patterns/entity-card.md +77 -77
  448. package/templates/skills/ui-components/patterns/grid-layout.md +91 -91
  449. package/templates/skills/ui-components/patterns/kanban.md +43 -43
  450. package/templates/skills/ui-components/responsive-guidelines.md +278 -278
  451. package/templates/skills/ui-components/style-guide.md +113 -113
  452. package/templates/skills/utils/SKILL.md +44 -44
  453. package/templates/skills/utils/subcommands/test-web-config.md +152 -152
  454. package/templates/skills/utils/subcommands/test-web.md +123 -123
  455. package/templates/skills/validate/SKILL.md +181 -181
  456. package/templates/skills/validate-feature/SKILL.md +101 -101
  457. package/templates/skills/validate-feature/references/api-smoke-tests.md +140 -140
  458. package/templates/skills/validate-feature/references/db-validation-checks.md +180 -180
  459. package/templates/skills/validate-feature/steps/step-00-dependencies.md +121 -121
  460. package/templates/skills/validate-feature/steps/step-01-compile.md +39 -39
  461. package/templates/skills/validate-feature/steps/step-02-unit-tests.md +45 -45
  462. package/templates/skills/validate-feature/steps/step-03-integration-tests.md +53 -53
  463. package/templates/skills/validate-feature/steps/step-04-api-smoke.md +94 -94
  464. package/templates/skills/validate-feature/steps/step-05-db-validation.md +149 -149
  465. package/templates/skills/workflow/SKILL.md +127 -127
  466. package/templates/skills/workflow/steps/step-00-init.md +57 -57
  467. package/templates/skills/workflow/steps/step-01-implementation.md +84 -84
  468. package/templates/test-web/api-health.json +38 -38
  469. package/templates/test-web/minimal.json +19 -19
  470. package/templates/test-web/npm-package.json +46 -46
  471. package/templates/test-web/seo-check.json +54 -54
@@ -1,1053 +1,1053 @@
1
- # SmartStack Domain API Reference
2
-
3
- > **Source of truth:** `SmartStack.app/src/SmartStack.Domain/Common/`
4
- > **Loaded by:** step-01 (analyze), step-03 (execute)
5
-
6
- ---
7
-
8
- ## BaseEntity
9
-
10
- ```csharp
11
- namespace SmartStack.Domain.Common;
12
-
13
- public abstract class BaseEntity
14
- {
15
- public Guid Id { get; set; }
16
- public DateTime CreatedAt { get; set; }
17
- public DateTime? UpdatedAt { get; set; }
18
- }
19
- ```
20
-
21
- **ONLY 3 properties.** No Code, no IsDeleted, no RowVersion, no SoftDelete, no CreatedBy/UpdatedBy.
22
-
23
- ---
24
-
25
- ## Interfaces
26
-
27
- ### ITenantEntity (mandatory tenant isolation)
28
-
29
- ```csharp
30
- public interface ITenantEntity
31
- {
32
- Guid TenantId { get; }
33
- }
34
- ```
35
-
36
- ### IAuditableEntity (audit trail)
37
-
38
- ```csharp
39
- public interface IAuditableEntity
40
- {
41
- string? CreatedBy { get; set; }
42
- string? UpdatedBy { get; set; }
43
- }
44
- ```
45
-
46
- ### IOptionalTenantEntity (nullable tenant)
47
-
48
- ```csharp
49
- public interface IOptionalTenantEntity
50
- {
51
- Guid? TenantId { get; }
52
- }
53
- ```
54
-
55
- ### IScopedTenantEntity (tenant + scope visibility)
56
-
57
- ```csharp
58
- public interface IScopedTenantEntity : IOptionalTenantEntity
59
- {
60
- EntityScope Scope { get; }
61
- }
62
- ```
63
-
64
- ### EntityScope enum
65
-
66
- ```csharp
67
- public enum EntityScope
68
- {
69
- Tenant = 0, // Visible only to specific tenant (TenantId required)
70
- Shared = 1, // Visible to all tenants (TenantId null)
71
- Platform = 2 // Visible only to platform admins (HasGlobalAccess)
72
- }
73
- ```
74
-
75
- ---
76
-
77
- ## Entity Pattern (tenant-scoped, most common)
78
-
79
- ```csharp
80
- using SmartStack.Domain.Common;
81
-
82
- namespace {ProjectName}.Domain.Entities.{App}.{Module};
83
-
84
- public class {Name} : BaseEntity, ITenantEntity, IAuditableEntity
85
- {
86
- // ITenantEntity
87
- public Guid TenantId { get; private set; }
88
-
89
- // IAuditableEntity
90
- public string? CreatedBy { get; set; }
91
- public string? UpdatedBy { get; set; }
92
-
93
- // Business properties (add your own)
94
- public string Code { get; private set; } = null!;
95
- public string Name { get; private set; } = null!;
96
- public string? Description { get; private set; }
97
- public bool IsActive { get; private set; } = true;
98
-
99
- private {Name}() { }
100
-
101
- public static {Name} Create(Guid tenantId, string code, string name)
102
- {
103
- if (tenantId == Guid.Empty)
104
- throw new ArgumentException("TenantId is required", nameof(tenantId));
105
-
106
- return new {Name}
107
- {
108
- Id = Guid.NewGuid(),
109
- TenantId = tenantId,
110
- Code = code.ToLowerInvariant(),
111
- Name = name,
112
- CreatedAt = DateTime.UtcNow
113
- };
114
- }
115
-
116
- public void Update(string name, string? description)
117
- {
118
- Name = name;
119
- Description = description;
120
- UpdatedAt = DateTime.UtcNow;
121
- }
122
- }
123
- ```
124
-
125
- ---
126
-
127
- ## Entity Pattern (platform-level, no tenant)
128
-
129
- ```csharp
130
- public class {Name} : BaseEntity, IAuditableEntity
131
- {
132
- public string? CreatedBy { get; set; }
133
- public string? UpdatedBy { get; set; }
134
-
135
- // Business properties
136
- public string Code { get; private set; } = null!;
137
- public string Name { get; private set; } = null!;
138
-
139
- private {Name}() { }
140
-
141
- public static {Name} Create(string code, string name)
142
- {
143
- return new {Name}
144
- {
145
- Id = Guid.NewGuid(),
146
- Code = code.ToLowerInvariant(),
147
- Name = name,
148
- CreatedAt = DateTime.UtcNow
149
- };
150
- }
151
- }
152
- ```
153
-
154
- ### Entity Pattern — Cross-Tenant (IOptionalTenantEntity)
155
-
156
- For entities that can be shared across tenants (e.g., Department, Currency). TenantId is nullable — null means shared, Guid means tenant-specific. The user decides the scope at creation time.
157
-
158
- ```csharp
159
- public class {Name} : BaseEntity, IOptionalTenantEntity, IAuditableEntity
160
- {
161
- // TenantId nullable — null = shared across all tenants
162
- public Guid? TenantId { get; private set; }
163
-
164
- public string? CreatedBy { get; set; }
165
- public string? UpdatedBy { get; set; }
166
-
167
- // Business properties
168
- public string Code { get; private set; } = string.Empty;
169
- public string Name { get; private set; } = string.Empty;
170
-
171
- private {Name}() { }
172
-
173
- /// <param name="tenantId">null = shared (cross-tenant), Guid = tenant-specific</param>
174
- public static {Name} Create(Guid? tenantId = null, string code, string name)
175
- {
176
- return new {Name}
177
- {
178
- Id = Guid.NewGuid(),
179
- TenantId = tenantId,
180
- Code = code.ToLowerInvariant(),
181
- Name = name,
182
- CreatedAt = DateTime.UtcNow
183
- };
184
- }
185
- }
186
- ```
187
-
188
- **EF Core global query filter (already in SmartStack.app CoreDbContext):**
189
- ```csharp
190
- builder.HasQueryFilter(e => !ShouldFilterByTenant || e.TenantId == null || e.TenantId == CurrentTenantId);
191
- ```
192
- This automatically includes shared (null) + current tenant data in all queries.
193
-
194
- **Service pattern for optional tenant:**
195
- ```csharp
196
- // No guard clause — tenantId is nullable
197
- var tenantId = _currentTenant.TenantId; // null = creating shared data
198
- var entity = Department.Create(tenantId, dto.Code, dto.Name);
199
- ```
200
-
201
- ### Entity Pattern — Scoped (IScopedTenantEntity)
202
-
203
- For entities with explicit visibility control via EntityScope enum (Tenant, Shared, Platform).
204
-
205
- ```csharp
206
- public class {Name} : BaseEntity, IScopedTenantEntity, IAuditableEntity
207
- {
208
- public Guid? TenantId { get; private set; }
209
- public EntityScope Scope { get; private set; }
210
-
211
- public string? CreatedBy { get; set; }
212
- public string? UpdatedBy { get; set; }
213
-
214
- private {Name}() { }
215
-
216
- public static {Name} Create(Guid? tenantId = null, EntityScope scope = EntityScope.Tenant)
217
- {
218
- if (scope == EntityScope.Tenant && tenantId == null)
219
- throw new ArgumentException("TenantId is required when scope is Tenant");
220
-
221
- return new {Name}
222
- {
223
- Id = Guid.NewGuid(),
224
- TenantId = tenantId,
225
- Scope = scope,
226
- CreatedAt = DateTime.UtcNow
227
- };
228
- }
229
- }
230
- ```
231
-
232
- ### MCP tenantMode Parameter
233
-
234
- When calling `scaffold_extension`, use the `tenantMode` parameter:
235
- - `strict` (default) — ITenantEntity, Guid TenantId (required)
236
- - `optional` — IOptionalTenantEntity, Guid? TenantId (cross-tenant)
237
- - `scoped` — IScopedTenantEntity, Guid? TenantId + EntityScope
238
- - `none` — No tenant interface (platform-level entities)
239
-
240
- The old `isSystemEntity: true` still works and maps to `tenantMode: 'none'`.
241
-
242
- ---
243
-
244
- ## EF Configuration Pattern
245
-
246
- ```csharp
247
- using Microsoft.EntityFrameworkCore;
248
- using Microsoft.EntityFrameworkCore.Metadata.Builders;
249
-
250
- public class {Name}Configuration : IEntityTypeConfiguration<{Name}>
251
- {
252
- public void Configure(EntityTypeBuilder<{Name}> builder)
253
- {
254
- builder.ToTable("{prefix}{Name}s", "{schema}");
255
-
256
- builder.HasKey(x => x.Id);
257
-
258
- // Tenant (if ITenantEntity)
259
- builder.Property(x => x.TenantId).IsRequired();
260
- builder.HasIndex(x => x.TenantId)
261
- .HasDatabaseName("IX_{prefix}{Name}s_TenantId");
262
-
263
- // Business properties
264
- builder.Property(x => x.Code).HasMaxLength(50).IsRequired();
265
- builder.Property(x => x.Name).HasMaxLength(100).IsRequired();
266
- builder.Property(x => x.Description).HasMaxLength(500);
267
-
268
- // Audit (from IAuditableEntity)
269
- builder.Property(x => x.CreatedBy).HasMaxLength(256);
270
- builder.Property(x => x.UpdatedBy).HasMaxLength(256);
271
-
272
- // Unique indexes
273
- builder.HasIndex(x => new { x.TenantId, x.Code })
274
- .IsUnique()
275
- .HasDatabaseName("IX_{prefix}{Name}s_Tenant_Code");
276
-
277
- // Relationships
278
- // builder.HasMany(x => x.Children)
279
- // .WithOne(x => x.Parent)
280
- // .HasForeignKey(x => x.ParentId)
281
- // .OnDelete(DeleteBehavior.Restrict);
282
-
283
- // Seed data (if applicable)
284
- // builder.HasData({Name}SeedData.GetSeedData());
285
- }
286
- }
287
- ```
288
-
289
- ---
290
-
291
- ## Service Pattern (tenant-scoped, MANDATORY)
292
-
293
- > **CRITICAL:** ALL services MUST inject `ICurrentUserService` + `ICurrentTenantService` and filter by `TenantId`. Missing TenantId = OWASP A01 vulnerability.
294
-
295
- ```csharp
296
- using Microsoft.EntityFrameworkCore;
297
- using Microsoft.Extensions.Logging;
298
- using SmartStack.Application.Common.Interfaces.Identity;
299
- using SmartStack.Application.Common.Interfaces.Tenants;
300
- using SmartStack.Application.Common.Interfaces.Persistence;
301
-
302
- namespace {ProjectName}.Infrastructure.Services.{App}.{Module};
303
-
304
- public class {Name}Service : I{Name}Service
305
- {
306
- private readonly IExtensionsDbContext _db;
307
- private readonly ICurrentUserService _currentUser;
308
- private readonly ICurrentTenantService _currentTenant;
309
- private readonly ILogger<{Name}Service> _logger;
310
-
311
- public {Name}Service(
312
- IExtensionsDbContext db,
313
- ICurrentUserService currentUser,
314
- ICurrentTenantService currentTenant,
315
- ILogger<{Name}Service> logger)
316
- {
317
- _db = db;
318
- _currentUser = currentUser;
319
- _currentTenant = currentTenant;
320
- _logger = logger;
321
- }
322
-
323
- public async Task<PaginatedResult<{Name}ResponseDto>> GetAllAsync(
324
- string? search = null,
325
- int page = 1,
326
- int pageSize = 20,
327
- CancellationToken ct = default)
328
- {
329
- // MANDATORY guard — throws 400 if no tenant context (e.g., missing X-Tenant-Slug header)
330
- var tenantId = _currentTenant.TenantId
331
- ?? throw new TenantContextRequiredException();
332
-
333
- var query = _db.{Name}s
334
- .Where(x => x.TenantId == tenantId) // MANDATORY tenant filter
335
- .AsNoTracking();
336
-
337
- // Search filter — enables EntityLookup on frontend
338
- if (!string.IsNullOrWhiteSpace(search))
339
- {
340
- query = query.Where(x =>
341
- x.Name.Contains(search) ||
342
- x.Code.Contains(search));
343
- }
344
-
345
- var totalCount = await query.CountAsync(ct);
346
- var items = await query
347
- .OrderBy(x => x.Name)
348
- .Skip((page - 1) * pageSize)
349
- .Take(pageSize)
350
- .Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
351
- .ToListAsync(ct);
352
-
353
- return new PaginatedResult<{Name}ResponseDto>(items, totalCount, page, pageSize);
354
- }
355
-
356
- public async Task<{Name}ResponseDto?> GetByIdAsync(Guid id, CancellationToken ct)
357
- {
358
- var tenantId = _currentTenant.TenantId
359
- ?? throw new TenantContextRequiredException();
360
-
361
- return await _db.{Name}s
362
- .Where(x => x.Id == id && x.TenantId == tenantId) // MANDATORY
363
- .AsNoTracking()
364
- .Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
365
- .FirstOrDefaultAsync(ct);
366
- }
367
-
368
- public async Task<{Name}ResponseDto> CreateAsync(Create{Name}Dto dto, CancellationToken ct)
369
- {
370
- var tenantId = _currentTenant.TenantId
371
- ?? throw new TenantContextRequiredException();
372
-
373
- var entity = {Name}.Create(
374
- tenantId: tenantId, // MANDATORY — never Guid.Empty
375
- code: dto.Code,
376
- name: dto.Name);
377
-
378
- entity.CreatedBy = _currentUser.UserId?.ToString();
379
-
380
- _db.{Name}s.Add(entity);
381
- await _db.SaveChangesAsync(ct);
382
-
383
- _logger.LogInformation("Created {Entity} {Id} for tenant {TenantId}",
384
- nameof({Name}), entity.Id, tenantId);
385
-
386
- return new {Name}ResponseDto(entity.Id, entity.Code, entity.Name, entity.CreatedAt);
387
- }
388
-
389
- public async Task DeleteAsync(Guid id, CancellationToken ct)
390
- {
391
- var tenantId = _currentTenant.TenantId
392
- ?? throw new TenantContextRequiredException();
393
-
394
- var entity = await _db.{Name}s
395
- .FirstOrDefaultAsync(x => x.Id == id && x.TenantId == tenantId, ct)
396
- ?? throw new KeyNotFoundException($"{Name} {id} not found");
397
-
398
- _db.{Name}s.Remove(entity);
399
- await _db.SaveChangesAsync(ct);
400
- }
401
- }
402
- ```
403
-
404
- **Key interfaces (from SmartStack NuGet package):**
405
- - `ICurrentUserService` (from `SmartStack.Application.Common.Interfaces.Identity`): provides `UserId` (Guid?), `Email` (string?), `IsAuthenticated` (bool)
406
- - `ICurrentTenantService` (from `SmartStack.Application.Common.Interfaces.Tenants`): provides `TenantId` (Guid?), `HasTenant` (bool), `TenantSlug` (string?)
407
- - `IExtensionsDbContext` (for client extensions) or `ICoreDbContext` (for platform)
408
-
409
- **MANDATORY guard clause (first line of every method):**
410
- ```csharp
411
- var tenantId = _currentTenant.TenantId
412
- ?? throw new TenantContextRequiredException();
413
- ```
414
- This converts a null TenantId into a clean 400 Bad Request response via `GlobalExceptionHandlerMiddleware`.
415
- **IMPORTANT:** Uses `TenantContextRequiredException` (400), NOT `UnauthorizedAccessException` (401). A missing tenant is a bad request, not an auth failure — the JWT is valid, `[Authorize]` passed.
416
-
417
- **FORBIDDEN in services:**
418
- - `_currentTenant.TenantId!.Value` — throws `InvalidOperationException` (500) instead of clean 400
419
- - `UnauthorizedAccessException("Tenant context is required")` — throws 401, triggers frontend token clearing
420
- - `tenantId: Guid.Empty` — always use validated tenantId from guard clause
421
- - Queries WITHOUT `.Where(x => x.TenantId == tenantId)` — data leak
422
- - Missing `ILogger<T>` — undiagnosable in production
423
- - Using `ICurrentUser` (does NOT exist) — use `ICurrentUserService` + `ICurrentTenantService`
424
-
425
- ---
426
-
427
- ## Controller Pattern (NavRoute)
428
-
429
- ```csharp
430
- using Microsoft.AspNetCore.Authorization;
431
- using Microsoft.AspNetCore.Mvc;
432
- using SmartStack.Api.Routing;
433
- using SmartStack.Api.Authorization;
434
-
435
- namespace {ProjectName}.Api.Controllers.{App};
436
-
437
- [ApiController]
438
- [NavRoute("{app}.{module}")]
439
- [Authorize]
440
- public class {Name}Controller : ControllerBase
441
- {
442
- private readonly I{Name}Service _service;
443
- private readonly ILogger<{Name}Controller> _logger;
444
-
445
- public {Name}Controller(I{Name}Service service, ILogger<{Name}Controller> logger)
446
- {
447
- _service = service;
448
- _logger = logger;
449
- }
450
-
451
- [HttpGet]
452
- [RequirePermission(Permissions.{Module}.Read)]
453
- public async Task<ActionResult<PaginatedResult<{Name}ResponseDto>>> GetAll(
454
- [FromQuery] string? search = null,
455
- [FromQuery] int page = 1,
456
- [FromQuery] int pageSize = 20,
457
- CancellationToken ct = default)
458
- => Ok(await _service.GetAllAsync(search, page, pageSize, ct));
459
-
460
- [HttpGet("{id:guid}")]
461
- [RequirePermission(Permissions.{Module}.Read)]
462
- public async Task<ActionResult<{Name}ResponseDto>> GetById(Guid id, CancellationToken ct)
463
- {
464
- var result = await _service.GetByIdAsync(id, ct);
465
- return result is null ? NotFound() : Ok(result);
466
- }
467
-
468
- [HttpPost]
469
- [RequirePermission(Permissions.{Module}.Create)]
470
- public async Task<ActionResult<{Name}ResponseDto>> Create([FromBody] Create{Name}Dto dto, CancellationToken ct)
471
- {
472
- var result = await _service.CreateAsync(dto, ct);
473
- return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
474
- }
475
-
476
- [HttpPut("{id:guid}")]
477
- [RequirePermission(Permissions.{Module}.Update)]
478
- public async Task<ActionResult<{Name}ResponseDto>> Update(Guid id, [FromBody] Update{Name}Dto dto, CancellationToken ct)
479
- {
480
- var result = await _service.UpdateAsync(id, dto, ct);
481
- return result is null ? NotFound() : Ok(result);
482
- }
483
-
484
- [HttpDelete("{id:guid}")]
485
- [RequirePermission(Permissions.{Module}.Delete)]
486
- public async Task<ActionResult> Delete(Guid id, CancellationToken ct)
487
- {
488
- await _service.DeleteAsync(id, ct);
489
- return NoContent();
490
- }
491
- }
492
- ```
493
-
494
- **CRITICAL — Route attribute rules:**
495
- - `[NavRoute]` is the ONLY route attribute needed — it resolves routes dynamically from Navigation entities at startup
496
- - **FORBIDDEN:** `[Route("api/...")]` alongside `[NavRoute]` — causes route conflicts and 404s at runtime
497
- - **FORBIDDEN:** `[Route("api/[controller]")]` — this is standard ASP.NET Core, NOT SmartStack
498
- - If a controller has `[NavRoute]`, there must be NO `[Route]` attribute on the class
499
-
500
- **CRITICAL:** Use `[RequirePermission(Permissions.{Module}.{Action})]` on EVERY endpoint — NEVER `[Authorize]` alone (no RBAC enforcement).
501
-
502
- **CRITICAL — Permission paths use IDENTICAL segments to NavRoute codes (kebab-case):**
503
- - NavRoute: `human-resources.employees` → Permission: `human-resources.employees.read`
504
- - NavRoute: `human-resources.employees.leaves` → Permission: `human-resources.employees.leaves.read`
505
- - FORBIDDEN: `humanresources.employees.read` (no kebab-case — mismatches NavRoute)
506
- - SmartStack.app convention: `support-client.my-tickets.read` (always kebab-case)
507
-
508
- ### Section-Level Controller (NavRoute with 4 segments)
509
-
510
- When a module has sections, each section gets its own controller with a 4-segment navRoute:
511
-
512
- ```csharp
513
- // Section-level controller: navRoute has 4 segments
514
- [ApiController]
515
- [NavRoute("{app}.{module}.{section}")]
516
- [Authorize]
517
- public class {Section}Controller : ControllerBase
518
- {
519
- // Example: human-resources.employees.departments
520
- [HttpGet]
521
- [RequirePermission(Permissions.{Section}.Read)]
522
- public async Task<ActionResult<PaginatedResult<{Section}ResponseDto>>> GetAll(
523
- [FromQuery] string? search = null,
524
- [FromQuery] int page = 1,
525
- [FromQuery] int pageSize = 20,
526
- CancellationToken ct = default)
527
- => Ok(await _service.GetAllAsync(search, page, pageSize, ct));
528
- }
529
- ```
530
-
531
- **NavRoute segment rules:**
532
- | Level | NavRoute format | Example |
533
- |-------|----------------|---------|
534
- | Module | `{app}.{module}` (2 segments) | `human-resources.employees` |
535
- | Section | `{app}.{module}.{section}` (3 segments) | `human-resources.employees.departments` |
536
-
537
- **Namespace:** `SmartStack.Api.Routing` (NOT `SmartStack.Api.Core.Routing`)
538
-
539
- **NavRoute resolves at startup from DB:** `administration.users` → `api/administration/users`
540
-
541
- ### Sub-Resource Pattern (NavRoute Suffix)
542
-
543
- When an entity is a child of another entity (e.g., LeaveTypes under Leaves), use `[NavRoute(..., Suffix = "types")]`:
544
-
545
- ```csharp
546
- // Sub-resource controller: types are nested under leaves
547
- [ApiController]
548
- [NavRoute("human-resources.employees.leaves", Suffix = "types")]
549
- [Authorize]
550
- public class LeaveTypesController : ControllerBase
551
- {
552
- [HttpGet]
553
- [RequirePermission(Permissions.Leaves.Read)] // inherits parent section permission
554
- public async Task<ActionResult<PaginatedResult<LeaveTypeResponseDto>>> GetAll(...)
555
- => Ok(await _service.GetAllAsync(search, page, pageSize, ct));
556
- }
557
- ```
558
-
559
- **Alternative pattern** (sub-resource endpoints within parent controller):
560
- ```csharp
561
- // LeaveTypes as endpoints within LeavesController
562
- [HttpGet("types")]
563
- [RequirePermission(Permissions.Leaves.Read)]
564
- public async Task<ActionResult<PaginatedResult<LeaveTypeResponseDto>>> GetAllLeaveTypes(...)
565
- ```
566
-
567
- > **CRITICAL — Sub-resource frontend completeness:**
568
- > If a parent page has a button (e.g., "Manage Leave Types") that `navigate()`s to a sub-resource route,
569
- > the frontend MUST include a page component for that route. Otherwise → dead link → white screen.
570
- > - Either create a dedicated sub-resource ListPage (e.g., `LeaveTypesPage.tsx`)
571
- > - Or DON'T include the navigate() button if pages won't be created
572
- > - **Prefer separate controllers** (with Suffix) over sub-endpoints in parent controller — easier to route
573
-
574
- ---
575
-
576
- ## Navigation Seed Data Pattern (CRITICAL — routes must be full paths)
577
-
578
- > **The navigation seed data defines menu routes stored in DB. These routes MUST be full paths starting with `/`.**
579
- > Short routes (e.g., `humanresources`) cause 400 Bad Request on application-tracking.
580
-
581
- ### Route Convention
582
-
583
- | Level | Route Format | Example |
584
- |-------|-------------|---------|
585
- | Application | `/{app-kebab}` | `/human-resources` |
586
- | Module | `/{app-kebab}/{module-kebab}` | `/human-resources/employees` |
587
- | Section | `/{app-kebab}/{module-kebab}/{section-kebab}` | `/human-resources/employees/departments` |
588
- | Resource | `/{app-kebab}/{module-kebab}/{section-kebab}/{resource-kebab}` | `/human-resources/employees/departments/export` |
589
-
590
- **ROUTE SPECIAL CASES (list and detail sections):**
591
- > The `list` and `detail` sections are NOT functional sub-areas — they are view modes of the module itself.
592
- > Their navigation routes MUST NOT add extra segments:
593
- > - `list` section route = module route (e.g., `/human-resources/employees`)
594
- > - `detail` section route = module route + `/:id` (e.g., `/human-resources/employees/:id`)
595
- > - FORBIDDEN: `/employees/list`, `/employees/detail/:id`
596
- > - Other sections (dashboard, approve, import, etc.) = module route + `/{section-kebab}` (normal behavior)
597
-
598
- **Rules:**
599
- - Routes ALWAYS start with `/`
600
- - Routes ALWAYS include the full hierarchy from application to current level
601
- - Routes ALWAYS use kebab-case (NOT PascalCase, NOT camelCase)
602
- - Code identifiers stay PascalCase in C# (`HumanResources`) but routes are kebab-case (`human-resources`)
603
-
604
- ### ToKebabCase Helper (include in SeedConstants or SeedDataProvider)
605
-
606
- ```csharp
607
- private static string ToKebabCase(string value)
608
- => System.Text.RegularExpressions.Regex
609
- .Replace(value, "([a-z])([A-Z])", "$1-$2")
610
- .ToLowerInvariant();
611
- ```
612
-
613
- ### SeedConstants Pattern
614
-
615
- ```csharp
616
- public static class SeedConstants
617
- {
618
- // Deterministic GUIDs (SHA256-based, reproducible across environments)
619
- // NOTE: Application/Module/Section/Resource IDs are deterministic.
620
- public static readonly Guid ApplicationId = DeterministicGuid("nav:human-resources");
621
- public static readonly Guid ModuleId = DeterministicGuid("nav:human-resources.employees");
622
- public static readonly Guid SectionId = DeterministicGuid("nav:human-resources.employees.departments");
623
-
624
- private static Guid DeterministicGuid(string input)
625
- {
626
- var hash = System.Security.Cryptography.SHA256.HashData(
627
- System.Text.Encoding.UTF8.GetBytes(input));
628
- var bytes = new byte[16];
629
- Array.Copy(hash, bytes, 16);
630
- bytes[6] = (byte)((bytes[6] & 0x0F) | 0x50); // version 5
631
- bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80); // variant
632
- return new Guid(bytes);
633
- }
634
- }
635
- ```
636
-
637
- ### Navigation Seed Data Example
638
-
639
- ```csharp
640
- // Application: /human-resources
641
- var app = NavigationApplication.Create(
642
- "human-resources", "Human Resources", "HR Management",
643
- "Users", IconType.Lucide,
644
- "/human-resources", // FULL PATH — starts with /, kebab-case
645
- 10);
646
-
647
- // Module: /human-resources/employees
648
- var module = NavigationModule.Create(
649
- app.Id, "employees", "Employees", "Employee management",
650
- "UserCheck", IconType.Lucide,
651
- "/human-resources/employees", // FULL PATH — includes parent
652
- 10);
653
-
654
- // Section: /human-resources/employees/departments
655
- var section = NavigationSection.Create(
656
- module.Id, "departments", "Departments", "Manage departments",
657
- "Building2", IconType.Lucide,
658
- "/human-resources/employees/departments", // FULL PATH
659
- 10);
660
- ```
661
-
662
- ### FORBIDDEN in Seed Data
663
-
664
- | Mistake | Reality |
665
- |---------|---------|
666
- | `"humanresources"` as route | Must be `"/human-resources"` (full path, kebab-case) |
667
- | `"employees"` as route | Must be `"/human-resources/employees"` (includes parent) |
668
- | `Guid.NewGuid()` in seed data | Must use deterministic GUIDs (SHA256) |
669
- | Missing translations | Must have 4 languages: fr, en, it, de |
670
- | Missing NavigationApplicationSeedData | Menu invisible without Application level |
671
-
672
- ---
673
-
674
- ## DbContext Pattern (extensions)
675
-
676
- ```csharp
677
- // In IExtensionsDbContext.cs:
678
- public DbSet<{Name}> {Name}s => Set<{Name}>();
679
-
680
- // In ExtensionsDbContext.cs (same line):
681
- public DbSet<{Name}> {Name}s => Set<{Name}>();
682
- ```
683
-
684
- ---
685
-
686
- ## DI Registration Pattern
687
-
688
- ```csharp
689
- // In DependencyInjection.cs or ServiceCollectionExtensions.cs:
690
- services.AddScoped<I{Name}Service, {Name}Service>();
691
- services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
692
- ```
693
-
694
- ---
695
-
696
- ## DTO Type Mapping (CRITICAL)
697
-
698
- > **Use the correct .NET type for each property.** Incorrect types cause runtime parsing errors.
699
-
700
- | Property Pattern | .NET Type | JSON Format | Example |
701
- |-----------------|-----------|-------------|---------|
702
- | `*Date`, `StartDate`, `EndDate`, `BirthDate` | `DateOnly` | `"2025-03-15"` | `public DateOnly Date { get; set; }` |
703
- | `CreatedAt`, `UpdatedAt` | `DateTime` | `"2025-03-15T10:30:00Z"` | `public DateTime CreatedAt { get; set; }` |
704
- | `*Time`, `StartTime` | `TimeOnly` | `"14:30:00"` | `public TimeOnly StartTime { get; set; }` |
705
- | Duration, hours | `decimal` | `8.5` | `public decimal HoursWorked { get; set; }` |
706
- | FK reference | `Guid` | `"uuid-string"` | `public Guid EmployeeId { get; set; }` |
707
-
708
- **FORBIDDEN in DTOs:**
709
- - `string Date` / `string StartDate` — use `DateOnly`
710
- - `string Time` — use `TimeOnly`
711
- - `DateTime BirthDate` — use `DateOnly` (no time component needed)
712
- - `int` for hours/duration — use `decimal` for fractional values
713
-
714
- ---
715
-
716
- ## Common Mistakes to Avoid
717
-
718
- | Mistake | Reality |
719
- |---------|---------|
720
- | `entity.SoftDelete()` | Does NOT exist — no soft delete in BaseEntity |
721
- | `entity.Code` inherited | Code is a business property — add it yourself |
722
- | `e.RowVersion` in config | Does NOT exist in BaseEntity |
723
- | `e.IsDeleted` filter | Does NOT exist — no soft delete |
724
- | `SmartStack.Api.Core.Routing` | Wrong — use `SmartStack.Api.Routing` |
725
- | `SystemEntity` base class | Does NOT exist — use `BaseEntity` for all |
726
- | `[Route("api/...")] + [NavRoute]` | **FORBIDDEN** — causes 404s. Only `[NavRoute]` needed (resolves route from DB at startup). Remove ALL `[Route]` attributes when `[NavRoute]` is present. |
727
- | `SmartStack.Domain.Common.Interfaces` | Wrong — interfaces are in `SmartStack.Domain.Common` directly |
728
- | `[Authorize]` without `[RequirePermission]` | No RBAC enforcement — always use `[RequirePermission]` |
729
- | `tenantId: Guid.Empty` in services | OWASP A01 — always use validated `_currentTenant.TenantId` |
730
- | Service without `ICurrentTenantService` | All tenant data leaks — inject `ICurrentTenantService` |
731
- | `ICurrentUser` in service code | Does NOT exist — use `ICurrentUserService` + `ICurrentTenantService` |
732
- | `_currentTenant.TenantId!.Value` | Crashes with 500 — use `?? throw new TenantContextRequiredException()` |
733
- | `UnauthorizedAccessException("Tenant context is required")` | Returns 401 → clears frontend token. Use `TenantContextRequiredException()` (400) |
734
- | Route `"humanresources"` in seed data | Must be full path `"/human-resources"` |
735
- | Route without leading `/` | All routes must start with `/` |
736
- | `humanresources.employees.read` in permissions | Permission segments MUST match NavRoute kebab-case: `human-resources.employees.read` |
737
- | `Permission.Create()` | Does NOT exist — use `CreateForModule()`, `CreateForSection()`, etc. |
738
- | `GetAllAsync()` without search param | ALL GetAll endpoints MUST support `?search=` for EntityLookup |
739
- | `string Date` in DTO | Date-only fields MUST use `DateOnly`, NEVER `string` |
740
- | `DateTime` for date-only | Use `DateOnly` when no time component needed |
741
- | FK field as plain text input | Frontend MUST use `EntityLookup` component for Guid FK fields |
742
- | `PagedResult<T>` / `PaginatedResultDto<T>` | FORBIDDEN — use `PaginatedResult<T>` only |
743
-
744
- ---
745
-
746
- ## PaginatedResult Pattern
747
-
748
- > **Canonical type for ALL paginated responses.** One name, one contract, everywhere.
749
-
750
- ### Definition (Backend — `SmartStack.Application.Common.Models`)
751
-
752
- ```csharp
753
- namespace SmartStack.Application.Common.Models;
754
-
755
- public record PaginatedResult<T>(
756
- List<T> Items,
757
- int TotalCount,
758
- int Page,
759
- int PageSize)
760
- {
761
- public int TotalPages => PageSize > 0
762
- ? (int)Math.Ceiling((double)TotalCount / PageSize) : 0;
763
- public bool HasPreviousPage => Page > 1;
764
- public bool HasNextPage => Page < TotalPages;
765
-
766
- public static PaginatedResult<T> Empty(int page = 1, int pageSize = 20)
767
- => new([], 0, page, pageSize);
768
- }
769
- ```
770
-
771
- ### Extension Method
772
-
773
- ```csharp
774
- namespace SmartStack.Application.Common.Extensions;
775
-
776
- public static class QueryableExtensions
777
- {
778
- public const int MaxPageSize = 100;
779
-
780
- public static async Task<PaginatedResult<T>> ToPaginatedResultAsync<T>(
781
- this IQueryable<T> query,
782
- int page = 1,
783
- int pageSize = 20,
784
- CancellationToken ct = default)
785
- {
786
- page = Math.Max(1, page);
787
- pageSize = Math.Clamp(pageSize, 1, MaxPageSize);
788
-
789
- var totalCount = await query.CountAsync(ct);
790
- var items = await query
791
- .Skip((page - 1) * pageSize)
792
- .Take(pageSize)
793
- .ToListAsync(ct);
794
-
795
- return new PaginatedResult<T>(items, totalCount, page, pageSize);
796
- }
797
- }
798
- ```
799
-
800
- ### Usage in Service (with search + extension method)
801
-
802
- ```csharp
803
- public async Task<PaginatedResult<{Name}ResponseDto>> GetAllAsync(
804
- string? search = null, int page = 1, int pageSize = 20, CancellationToken ct = default)
805
- {
806
- var tenantId = _currentTenant.TenantId
807
- ?? throw new TenantContextRequiredException();
808
-
809
- var query = _db.{Name}s
810
- .Where(x => x.TenantId == tenantId)
811
- .AsNoTracking();
812
-
813
- if (!string.IsNullOrWhiteSpace(search))
814
- query = query.Where(x => x.Name.Contains(search) || x.Code.Contains(search));
815
-
816
- return await query
817
- .OrderBy(x => x.Name)
818
- .Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
819
- .ToPaginatedResultAsync(page, pageSize, ct);
820
- }
821
- ```
822
-
823
- ### Frontend Types (`@/types/pagination.ts`)
824
-
825
- ```typescript
826
- export interface PaginatedResult<T> {
827
- items: T[];
828
- totalCount: number;
829
- page: number;
830
- pageSize: number;
831
- totalPages: number;
832
- hasPreviousPage: boolean;
833
- hasNextPage: boolean;
834
- }
835
-
836
- export interface PaginationParams {
837
- page?: number;
838
- pageSize?: number;
839
- search?: string;
840
- sortBy?: string;
841
- sortDirection?: 'asc' | 'desc';
842
- }
843
- ```
844
-
845
- ### FORBIDDEN Type Names
846
-
847
- | Forbidden | Canonical Replacement |
848
- |-----------|----------------------|
849
- | `PagedResult<T>` | `PaginatedResult<T>` |
850
- | `PaginatedResultDto<T>` | `PaginatedResult<T>` |
851
- | `PaginatedResponse<T>` | `PaginatedResult<T>` |
852
- | `PageResultDto<T>` | `PaginatedResult<T>` |
853
- | `PaginatedRequest` | `PaginationParams` |
854
- | `QueryParameters` | `PaginationParams` |
855
- | `currentPage` (property) | `page` |
856
- | `HasPrevious` (property) | `HasPreviousPage` |
857
- | `HasNext` (property) | `HasNextPage` |
858
-
859
- ### Rules
860
-
861
- - **Max pageSize = 100** — enforced via `Math.Clamp(pageSize, 1, 100)` or extension method
862
- - **Default page = 1, pageSize = 20** — all GetAll endpoints
863
- - **Search param mandatory** — enables `EntityLookup` on frontend
864
- - **POST-CHECK 16** blocks `List<T>` returns on GetAll
865
- - **POST-CHECK 31** blocks non-canonical pagination type names
866
-
867
- ---
868
-
869
- ## Critical Anti-Patterns (with code examples)
870
-
871
- > **These are the most common and dangerous mistakes.** Each one has been observed in production code generation.
872
-
873
- ### Anti-Pattern 1: HasQueryFilter with `!= Guid.Empty` (SECURITY — OWASP A01)
874
-
875
- The `HasQueryFilter` in EF Core should use **runtime tenant resolution**, NOT a static comparison against `Guid.Empty`.
876
-
877
- **INCORRECT — Does NOT isolate tenants:**
878
- ```csharp
879
- // WRONG: This only excludes empty GUIDs — ALL tenant data is still visible to everyone!
880
- public void Configure(EntityTypeBuilder<MyEntity> builder)
881
- {
882
- builder.HasQueryFilter(e => e.TenantId != Guid.Empty);
883
- }
884
- ```
885
-
886
- **CORRECT — Tenant isolation via service:**
887
- ```csharp
888
- // CORRECT: In SmartStack, tenant filtering is done in the SERVICE layer, not via HasQueryFilter.
889
- public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(...)
890
- {
891
- var query = _db.MyEntities
892
- .Where(x => x.TenantId == _currentUser.TenantId) // MANDATORY runtime filter
893
- .AsNoTracking();
894
- // ...
895
- }
896
- ```
897
-
898
- **Why it's wrong:** `HasQueryFilter(e => e.TenantId != Guid.Empty)` is a **static filter** — it only removes records with empty GUIDs. It does NOT restrict data to the current tenant. This is an **OWASP A01 Broken Access Control** vulnerability.
899
-
900
- ---
901
-
902
- ### Anti-Pattern 2: `List<T>` instead of `PaginatedResult<T>` for GetAll
903
-
904
- **INCORRECT — No pagination:**
905
- ```csharp
906
- // WRONG: Returns all records at once — no pagination, no totalCount
907
- public async Task<List<MyEntityDto>> GetAllAsync(CancellationToken ct)
908
- {
909
- return await _db.MyEntities
910
- .Where(x => x.TenantId == _currentUser.TenantId)
911
- .Select(x => new MyEntityDto(x.Id, x.Code, x.Name))
912
- .ToListAsync(ct);
913
- }
914
- ```
915
-
916
- **CORRECT — Paginated with search:**
917
- ```csharp
918
- // CORRECT: Returns PaginatedResult<T> with search, page, pageSize
919
- public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(
920
- string? search = null, int page = 1, int pageSize = 20, CancellationToken ct = default)
921
- {
922
- var query = _db.MyEntities.Where(x => x.TenantId == _currentUser.TenantId).AsNoTracking();
923
- if (!string.IsNullOrWhiteSpace(search))
924
- query = query.Where(x => x.Name.Contains(search) || x.Code.Contains(search));
925
- var totalCount = await query.CountAsync(ct);
926
- var items = await query.OrderBy(x => x.Name).Skip((page - 1) * pageSize).Take(pageSize)
927
- .Select(x => new MyEntityDto(x.Id, x.Code, x.Name, x.CreatedAt)).ToListAsync(ct);
928
- return new PaginatedResult<MyEntityDto>(items, totalCount, page, pageSize);
929
- }
930
- ```
931
-
932
- **Why it's wrong:** `List<T>` loads ALL records into memory. It also breaks `EntityLookup` which requires `{ items, totalCount }` response format.
933
-
934
- ---
935
-
936
- ### Anti-Pattern 3: Missing `IAuditableEntity` on tenant entities
937
-
938
- **INCORRECT — No audit trail:**
939
- ```csharp
940
- // WRONG: Tenant entity without IAuditableEntity
941
- public class MyEntity : BaseEntity, ITenantEntity
942
- {
943
- public Guid TenantId { get; private set; }
944
- public string Code { get; private set; } = null!;
945
- }
946
- ```
947
-
948
- **CORRECT — Always pair ITenantEntity with IAuditableEntity:**
949
- ```csharp
950
- public class MyEntity : BaseEntity, ITenantEntity, IAuditableEntity
951
- {
952
- public Guid TenantId { get; private set; }
953
- public string? CreatedBy { get; set; }
954
- public string? UpdatedBy { get; set; }
955
- public string Code { get; private set; } = null!;
956
- }
957
- ```
958
-
959
- **Why it's wrong:** Without `IAuditableEntity`, there is no record of who created or modified data. Mandatory for compliance in multi-tenant environments.
960
-
961
- ---
962
-
963
- ### Anti-Pattern 4: Code auto-generation with `Count() + 1`
964
-
965
- **INCORRECT — Race condition:**
966
- ```csharp
967
- // WRONG: Two concurrent requests get the same count
968
- var count = await _db.MyEntities.Where(x => x.TenantId == tenantId).CountAsync(ct);
969
- return $"emp-{(count + 1):D5}";
970
- ```
971
-
972
- **CORRECT — Use `ICodeGenerator<T>.NextCodeAsync()` (atomic with retry):**
973
- ```csharp
974
- private readonly ICodeGenerator<MyEntity> _codeGenerator;
975
-
976
- // In CreateAsync:
977
- var code = await _codeGenerator.NextCodeAsync(ct);
978
- var entity = MyEntity.Create(tenantId, code, dto.Name, createdBy: null);
979
- ```
980
-
981
- **Why it's wrong:** `Count() + 1` causes **race conditions** — concurrent requests generate duplicate codes. `ICodeGenerator<T>` uses `OrderByDescending` on existing codes + retry on unique constraint violation for safe concurrency.
982
-
983
- **Key rules when using auto-generated codes:**
984
- - **REMOVE** `Code` from `CreateDto` (auto-generated, not user-provided)
985
- - **KEEP** `Code` in `ResponseDto` (returned to frontend)
986
- - Register `ICodeGenerator<T>` in DI with `CodePatternConfig`
987
- - Code regex in validators: `^[a-z0-9_-]+$` (supports hyphens)
988
-
989
- **Full reference:** See `references/code-generation.md` for strategies (sequential, timestamp, yearly, UUID), volume-to-digits calculation, and complete implementation patterns.
990
-
991
- ---
992
-
993
- ### Anti-Pattern 5: Missing Update validator
994
-
995
- **INCORRECT — Only CreateValidator:**
996
- ```csharp
997
- public class CreateMyEntityDtoValidator : AbstractValidator<CreateMyEntityDto>
998
- {
999
- public CreateMyEntityDtoValidator()
1000
- {
1001
- RuleFor(x => x.Code).NotEmpty().MaximumLength(100);
1002
- RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
1003
- }
1004
- }
1005
- // No UpdateMyEntityDtoValidator exists!
1006
- ```
1007
-
1008
- **CORRECT — Always create validators in pairs:**
1009
- ```csharp
1010
- public class CreateMyEntityDtoValidator : AbstractValidator<CreateMyEntityDto> { /* ... */ }
1011
- public class UpdateMyEntityDtoValidator : AbstractValidator<UpdateMyEntityDto>
1012
- {
1013
- public UpdateMyEntityDtoValidator()
1014
- {
1015
- RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
1016
- }
1017
- }
1018
- ```
1019
-
1020
- **Why it's wrong:** Without an `UpdateValidator`, the Update endpoint accepts **any data without validation**.
1021
-
1022
- ---
1023
-
1024
- ### Anti-Pattern 6: `TenantId!.Value` null-forgiving operator (RUNTIME CRASH)
1025
-
1026
- The `!` (null-forgiving) operator followed by `.Value` on a `Guid?` suppresses compiler warnings but **throws `InvalidOperationException` at runtime** when TenantId is null.
1027
-
1028
- **INCORRECT — Crashes with 500 Internal Server Error:**
1029
- ```csharp
1030
- // WRONG: Throws InvalidOperationException("Nullable object must have a value") → 500 error
1031
- public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(...)
1032
- {
1033
- var tenantId = _currentTenant.TenantId!.Value; // CRASH if no tenant context
1034
- var query = _db.MyEntities.Where(x => x.TenantId == tenantId);
1035
- // ...
1036
- }
1037
- ```
1038
-
1039
- **CORRECT — Clean 400 via GlobalExceptionHandlerMiddleware:**
1040
- ```csharp
1041
- // CORRECT: Throws TenantContextRequiredException → middleware converts to 400 Bad Request
1042
- public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(...)
1043
- {
1044
- var tenantId = _currentTenant.TenantId
1045
- ?? throw new TenantContextRequiredException();
1046
- var query = _db.MyEntities.Where(x => x.TenantId == tenantId);
1047
- // ...
1048
- }
1049
- ```
1050
-
1051
- **Why `!.Value` is wrong:** When a user hits an API via Swagger with a valid JWT but no tenant context (missing `X-Tenant-Slug` header), `TenantId` is null. The `!.Value` pattern produces an opaque `500 Internal Server Error` instead of a clear `400 Bad Request` with an actionable message.
1052
-
1053
- **Why `UnauthorizedAccessException` is wrong:** A missing tenant is NOT an auth failure — the JWT is valid, `[Authorize]` passed. Using `UnauthorizedAccessException` returns 401, which triggers the frontend interceptor to clear the token and redirect to login. Use `TenantContextRequiredException` instead (returns 400, does not clear the token).
1
+ # SmartStack Domain API Reference
2
+
3
+ > **Source of truth:** `SmartStack.app/src/SmartStack.Domain/Common/`
4
+ > **Loaded by:** step-01 (analyze), step-03 (execute)
5
+
6
+ ---
7
+
8
+ ## BaseEntity
9
+
10
+ ```csharp
11
+ namespace SmartStack.Domain.Common;
12
+
13
+ public abstract class BaseEntity
14
+ {
15
+ public Guid Id { get; set; }
16
+ public DateTime CreatedAt { get; set; }
17
+ public DateTime? UpdatedAt { get; set; }
18
+ }
19
+ ```
20
+
21
+ **ONLY 3 properties.** No Code, no IsDeleted, no RowVersion, no SoftDelete, no CreatedBy/UpdatedBy.
22
+
23
+ ---
24
+
25
+ ## Interfaces
26
+
27
+ ### ITenantEntity (mandatory tenant isolation)
28
+
29
+ ```csharp
30
+ public interface ITenantEntity
31
+ {
32
+ Guid TenantId { get; }
33
+ }
34
+ ```
35
+
36
+ ### IAuditableEntity (audit trail)
37
+
38
+ ```csharp
39
+ public interface IAuditableEntity
40
+ {
41
+ string? CreatedBy { get; set; }
42
+ string? UpdatedBy { get; set; }
43
+ }
44
+ ```
45
+
46
+ ### IOptionalTenantEntity (nullable tenant)
47
+
48
+ ```csharp
49
+ public interface IOptionalTenantEntity
50
+ {
51
+ Guid? TenantId { get; }
52
+ }
53
+ ```
54
+
55
+ ### IScopedTenantEntity (tenant + scope visibility)
56
+
57
+ ```csharp
58
+ public interface IScopedTenantEntity : IOptionalTenantEntity
59
+ {
60
+ EntityScope Scope { get; }
61
+ }
62
+ ```
63
+
64
+ ### EntityScope enum
65
+
66
+ ```csharp
67
+ public enum EntityScope
68
+ {
69
+ Tenant = 0, // Visible only to specific tenant (TenantId required)
70
+ Shared = 1, // Visible to all tenants (TenantId null)
71
+ Platform = 2 // Visible only to platform admins (HasGlobalAccess)
72
+ }
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Entity Pattern (tenant-scoped, most common)
78
+
79
+ ```csharp
80
+ using SmartStack.Domain.Common;
81
+
82
+ namespace {ProjectName}.Domain.Entities.{App}.{Module};
83
+
84
+ public class {Name} : BaseEntity, ITenantEntity, IAuditableEntity
85
+ {
86
+ // ITenantEntity
87
+ public Guid TenantId { get; private set; }
88
+
89
+ // IAuditableEntity
90
+ public string? CreatedBy { get; set; }
91
+ public string? UpdatedBy { get; set; }
92
+
93
+ // Business properties (add your own)
94
+ public string Code { get; private set; } = null!;
95
+ public string Name { get; private set; } = null!;
96
+ public string? Description { get; private set; }
97
+ public bool IsActive { get; private set; } = true;
98
+
99
+ private {Name}() { }
100
+
101
+ public static {Name} Create(Guid tenantId, string code, string name)
102
+ {
103
+ if (tenantId == Guid.Empty)
104
+ throw new ArgumentException("TenantId is required", nameof(tenantId));
105
+
106
+ return new {Name}
107
+ {
108
+ Id = Guid.NewGuid(),
109
+ TenantId = tenantId,
110
+ Code = code.ToLowerInvariant(),
111
+ Name = name,
112
+ CreatedAt = DateTime.UtcNow
113
+ };
114
+ }
115
+
116
+ public void Update(string name, string? description)
117
+ {
118
+ Name = name;
119
+ Description = description;
120
+ UpdatedAt = DateTime.UtcNow;
121
+ }
122
+ }
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Entity Pattern (platform-level, no tenant)
128
+
129
+ ```csharp
130
+ public class {Name} : BaseEntity, IAuditableEntity
131
+ {
132
+ public string? CreatedBy { get; set; }
133
+ public string? UpdatedBy { get; set; }
134
+
135
+ // Business properties
136
+ public string Code { get; private set; } = null!;
137
+ public string Name { get; private set; } = null!;
138
+
139
+ private {Name}() { }
140
+
141
+ public static {Name} Create(string code, string name)
142
+ {
143
+ return new {Name}
144
+ {
145
+ Id = Guid.NewGuid(),
146
+ Code = code.ToLowerInvariant(),
147
+ Name = name,
148
+ CreatedAt = DateTime.UtcNow
149
+ };
150
+ }
151
+ }
152
+ ```
153
+
154
+ ### Entity Pattern — Cross-Tenant (IOptionalTenantEntity)
155
+
156
+ For entities that can be shared across tenants (e.g., Department, Currency). TenantId is nullable — null means shared, Guid means tenant-specific. The user decides the scope at creation time.
157
+
158
+ ```csharp
159
+ public class {Name} : BaseEntity, IOptionalTenantEntity, IAuditableEntity
160
+ {
161
+ // TenantId nullable — null = shared across all tenants
162
+ public Guid? TenantId { get; private set; }
163
+
164
+ public string? CreatedBy { get; set; }
165
+ public string? UpdatedBy { get; set; }
166
+
167
+ // Business properties
168
+ public string Code { get; private set; } = string.Empty;
169
+ public string Name { get; private set; } = string.Empty;
170
+
171
+ private {Name}() { }
172
+
173
+ /// <param name="tenantId">null = shared (cross-tenant), Guid = tenant-specific</param>
174
+ public static {Name} Create(Guid? tenantId = null, string code, string name)
175
+ {
176
+ return new {Name}
177
+ {
178
+ Id = Guid.NewGuid(),
179
+ TenantId = tenantId,
180
+ Code = code.ToLowerInvariant(),
181
+ Name = name,
182
+ CreatedAt = DateTime.UtcNow
183
+ };
184
+ }
185
+ }
186
+ ```
187
+
188
+ **EF Core global query filter (already in SmartStack.app CoreDbContext):**
189
+ ```csharp
190
+ builder.HasQueryFilter(e => !ShouldFilterByTenant || e.TenantId == null || e.TenantId == CurrentTenantId);
191
+ ```
192
+ This automatically includes shared (null) + current tenant data in all queries.
193
+
194
+ **Service pattern for optional tenant:**
195
+ ```csharp
196
+ // No guard clause — tenantId is nullable
197
+ var tenantId = _currentTenant.TenantId; // null = creating shared data
198
+ var entity = Department.Create(tenantId, dto.Code, dto.Name);
199
+ ```
200
+
201
+ ### Entity Pattern — Scoped (IScopedTenantEntity)
202
+
203
+ For entities with explicit visibility control via EntityScope enum (Tenant, Shared, Platform).
204
+
205
+ ```csharp
206
+ public class {Name} : BaseEntity, IScopedTenantEntity, IAuditableEntity
207
+ {
208
+ public Guid? TenantId { get; private set; }
209
+ public EntityScope Scope { get; private set; }
210
+
211
+ public string? CreatedBy { get; set; }
212
+ public string? UpdatedBy { get; set; }
213
+
214
+ private {Name}() { }
215
+
216
+ public static {Name} Create(Guid? tenantId = null, EntityScope scope = EntityScope.Tenant)
217
+ {
218
+ if (scope == EntityScope.Tenant && tenantId == null)
219
+ throw new ArgumentException("TenantId is required when scope is Tenant");
220
+
221
+ return new {Name}
222
+ {
223
+ Id = Guid.NewGuid(),
224
+ TenantId = tenantId,
225
+ Scope = scope,
226
+ CreatedAt = DateTime.UtcNow
227
+ };
228
+ }
229
+ }
230
+ ```
231
+
232
+ ### MCP tenantMode Parameter
233
+
234
+ When calling `scaffold_extension`, use the `tenantMode` parameter:
235
+ - `strict` (default) — ITenantEntity, Guid TenantId (required)
236
+ - `optional` — IOptionalTenantEntity, Guid? TenantId (cross-tenant)
237
+ - `scoped` — IScopedTenantEntity, Guid? TenantId + EntityScope
238
+ - `none` — No tenant interface (platform-level entities)
239
+
240
+ The old `isSystemEntity: true` still works and maps to `tenantMode: 'none'`.
241
+
242
+ ---
243
+
244
+ ## EF Configuration Pattern
245
+
246
+ ```csharp
247
+ using Microsoft.EntityFrameworkCore;
248
+ using Microsoft.EntityFrameworkCore.Metadata.Builders;
249
+
250
+ public class {Name}Configuration : IEntityTypeConfiguration<{Name}>
251
+ {
252
+ public void Configure(EntityTypeBuilder<{Name}> builder)
253
+ {
254
+ builder.ToTable("{prefix}{Name}s", "{schema}");
255
+
256
+ builder.HasKey(x => x.Id);
257
+
258
+ // Tenant (if ITenantEntity)
259
+ builder.Property(x => x.TenantId).IsRequired();
260
+ builder.HasIndex(x => x.TenantId)
261
+ .HasDatabaseName("IX_{prefix}{Name}s_TenantId");
262
+
263
+ // Business properties
264
+ builder.Property(x => x.Code).HasMaxLength(50).IsRequired();
265
+ builder.Property(x => x.Name).HasMaxLength(100).IsRequired();
266
+ builder.Property(x => x.Description).HasMaxLength(500);
267
+
268
+ // Audit (from IAuditableEntity)
269
+ builder.Property(x => x.CreatedBy).HasMaxLength(256);
270
+ builder.Property(x => x.UpdatedBy).HasMaxLength(256);
271
+
272
+ // Unique indexes
273
+ builder.HasIndex(x => new { x.TenantId, x.Code })
274
+ .IsUnique()
275
+ .HasDatabaseName("IX_{prefix}{Name}s_Tenant_Code");
276
+
277
+ // Relationships
278
+ // builder.HasMany(x => x.Children)
279
+ // .WithOne(x => x.Parent)
280
+ // .HasForeignKey(x => x.ParentId)
281
+ // .OnDelete(DeleteBehavior.Restrict);
282
+
283
+ // Seed data (if applicable)
284
+ // builder.HasData({Name}SeedData.GetSeedData());
285
+ }
286
+ }
287
+ ```
288
+
289
+ ---
290
+
291
+ ## Service Pattern (tenant-scoped, MANDATORY)
292
+
293
+ > **CRITICAL:** ALL services MUST inject `ICurrentUserService` + `ICurrentTenantService` and filter by `TenantId`. Missing TenantId = OWASP A01 vulnerability.
294
+
295
+ ```csharp
296
+ using Microsoft.EntityFrameworkCore;
297
+ using Microsoft.Extensions.Logging;
298
+ using SmartStack.Application.Common.Interfaces.Identity;
299
+ using SmartStack.Application.Common.Interfaces.Tenants;
300
+ using SmartStack.Application.Common.Interfaces.Persistence;
301
+
302
+ namespace {ProjectName}.Infrastructure.Services.{App}.{Module};
303
+
304
+ public class {Name}Service : I{Name}Service
305
+ {
306
+ private readonly IExtensionsDbContext _db;
307
+ private readonly ICurrentUserService _currentUser;
308
+ private readonly ICurrentTenantService _currentTenant;
309
+ private readonly ILogger<{Name}Service> _logger;
310
+
311
+ public {Name}Service(
312
+ IExtensionsDbContext db,
313
+ ICurrentUserService currentUser,
314
+ ICurrentTenantService currentTenant,
315
+ ILogger<{Name}Service> logger)
316
+ {
317
+ _db = db;
318
+ _currentUser = currentUser;
319
+ _currentTenant = currentTenant;
320
+ _logger = logger;
321
+ }
322
+
323
+ public async Task<PaginatedResult<{Name}ResponseDto>> GetAllAsync(
324
+ string? search = null,
325
+ int page = 1,
326
+ int pageSize = 20,
327
+ CancellationToken ct = default)
328
+ {
329
+ // MANDATORY guard — throws 400 if no tenant context (e.g., missing X-Tenant-Slug header)
330
+ var tenantId = _currentTenant.TenantId
331
+ ?? throw new TenantContextRequiredException();
332
+
333
+ var query = _db.{Name}s
334
+ .Where(x => x.TenantId == tenantId) // MANDATORY tenant filter
335
+ .AsNoTracking();
336
+
337
+ // Search filter — enables EntityLookup on frontend
338
+ if (!string.IsNullOrWhiteSpace(search))
339
+ {
340
+ query = query.Where(x =>
341
+ x.Name.Contains(search) ||
342
+ x.Code.Contains(search));
343
+ }
344
+
345
+ var totalCount = await query.CountAsync(ct);
346
+ var items = await query
347
+ .OrderBy(x => x.Name)
348
+ .Skip((page - 1) * pageSize)
349
+ .Take(pageSize)
350
+ .Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
351
+ .ToListAsync(ct);
352
+
353
+ return new PaginatedResult<{Name}ResponseDto>(items, totalCount, page, pageSize);
354
+ }
355
+
356
+ public async Task<{Name}ResponseDto?> GetByIdAsync(Guid id, CancellationToken ct)
357
+ {
358
+ var tenantId = _currentTenant.TenantId
359
+ ?? throw new TenantContextRequiredException();
360
+
361
+ return await _db.{Name}s
362
+ .Where(x => x.Id == id && x.TenantId == tenantId) // MANDATORY
363
+ .AsNoTracking()
364
+ .Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
365
+ .FirstOrDefaultAsync(ct);
366
+ }
367
+
368
+ public async Task<{Name}ResponseDto> CreateAsync(Create{Name}Dto dto, CancellationToken ct)
369
+ {
370
+ var tenantId = _currentTenant.TenantId
371
+ ?? throw new TenantContextRequiredException();
372
+
373
+ var entity = {Name}.Create(
374
+ tenantId: tenantId, // MANDATORY — never Guid.Empty
375
+ code: dto.Code,
376
+ name: dto.Name);
377
+
378
+ entity.CreatedBy = _currentUser.UserId?.ToString();
379
+
380
+ _db.{Name}s.Add(entity);
381
+ await _db.SaveChangesAsync(ct);
382
+
383
+ _logger.LogInformation("Created {Entity} {Id} for tenant {TenantId}",
384
+ nameof({Name}), entity.Id, tenantId);
385
+
386
+ return new {Name}ResponseDto(entity.Id, entity.Code, entity.Name, entity.CreatedAt);
387
+ }
388
+
389
+ public async Task DeleteAsync(Guid id, CancellationToken ct)
390
+ {
391
+ var tenantId = _currentTenant.TenantId
392
+ ?? throw new TenantContextRequiredException();
393
+
394
+ var entity = await _db.{Name}s
395
+ .FirstOrDefaultAsync(x => x.Id == id && x.TenantId == tenantId, ct)
396
+ ?? throw new KeyNotFoundException($"{Name} {id} not found");
397
+
398
+ _db.{Name}s.Remove(entity);
399
+ await _db.SaveChangesAsync(ct);
400
+ }
401
+ }
402
+ ```
403
+
404
+ **Key interfaces (from SmartStack NuGet package):**
405
+ - `ICurrentUserService` (from `SmartStack.Application.Common.Interfaces.Identity`): provides `UserId` (Guid?), `Email` (string?), `IsAuthenticated` (bool)
406
+ - `ICurrentTenantService` (from `SmartStack.Application.Common.Interfaces.Tenants`): provides `TenantId` (Guid?), `HasTenant` (bool), `TenantSlug` (string?)
407
+ - `IExtensionsDbContext` (for client extensions) or `ICoreDbContext` (for platform)
408
+
409
+ **MANDATORY guard clause (first line of every method):**
410
+ ```csharp
411
+ var tenantId = _currentTenant.TenantId
412
+ ?? throw new TenantContextRequiredException();
413
+ ```
414
+ This converts a null TenantId into a clean 400 Bad Request response via `GlobalExceptionHandlerMiddleware`.
415
+ **IMPORTANT:** Uses `TenantContextRequiredException` (400), NOT `UnauthorizedAccessException` (401). A missing tenant is a bad request, not an auth failure — the JWT is valid, `[Authorize]` passed.
416
+
417
+ **FORBIDDEN in services:**
418
+ - `_currentTenant.TenantId!.Value` — throws `InvalidOperationException` (500) instead of clean 400
419
+ - `UnauthorizedAccessException("Tenant context is required")` — throws 401, triggers frontend token clearing
420
+ - `tenantId: Guid.Empty` — always use validated tenantId from guard clause
421
+ - Queries WITHOUT `.Where(x => x.TenantId == tenantId)` — data leak
422
+ - Missing `ILogger<T>` — undiagnosable in production
423
+ - Using `ICurrentUser` (does NOT exist) — use `ICurrentUserService` + `ICurrentTenantService`
424
+
425
+ ---
426
+
427
+ ## Controller Pattern (NavRoute)
428
+
429
+ ```csharp
430
+ using Microsoft.AspNetCore.Authorization;
431
+ using Microsoft.AspNetCore.Mvc;
432
+ using SmartStack.Api.Routing;
433
+ using SmartStack.Api.Authorization;
434
+
435
+ namespace {ProjectName}.Api.Controllers.{App};
436
+
437
+ [ApiController]
438
+ [NavRoute("{app}.{module}")]
439
+ [Authorize]
440
+ public class {Name}Controller : ControllerBase
441
+ {
442
+ private readonly I{Name}Service _service;
443
+ private readonly ILogger<{Name}Controller> _logger;
444
+
445
+ public {Name}Controller(I{Name}Service service, ILogger<{Name}Controller> logger)
446
+ {
447
+ _service = service;
448
+ _logger = logger;
449
+ }
450
+
451
+ [HttpGet]
452
+ [RequirePermission(Permissions.{Module}.Read)]
453
+ public async Task<ActionResult<PaginatedResult<{Name}ResponseDto>>> GetAll(
454
+ [FromQuery] string? search = null,
455
+ [FromQuery] int page = 1,
456
+ [FromQuery] int pageSize = 20,
457
+ CancellationToken ct = default)
458
+ => Ok(await _service.GetAllAsync(search, page, pageSize, ct));
459
+
460
+ [HttpGet("{id:guid}")]
461
+ [RequirePermission(Permissions.{Module}.Read)]
462
+ public async Task<ActionResult<{Name}ResponseDto>> GetById(Guid id, CancellationToken ct)
463
+ {
464
+ var result = await _service.GetByIdAsync(id, ct);
465
+ return result is null ? NotFound() : Ok(result);
466
+ }
467
+
468
+ [HttpPost]
469
+ [RequirePermission(Permissions.{Module}.Create)]
470
+ public async Task<ActionResult<{Name}ResponseDto>> Create([FromBody] Create{Name}Dto dto, CancellationToken ct)
471
+ {
472
+ var result = await _service.CreateAsync(dto, ct);
473
+ return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
474
+ }
475
+
476
+ [HttpPut("{id:guid}")]
477
+ [RequirePermission(Permissions.{Module}.Update)]
478
+ public async Task<ActionResult<{Name}ResponseDto>> Update(Guid id, [FromBody] Update{Name}Dto dto, CancellationToken ct)
479
+ {
480
+ var result = await _service.UpdateAsync(id, dto, ct);
481
+ return result is null ? NotFound() : Ok(result);
482
+ }
483
+
484
+ [HttpDelete("{id:guid}")]
485
+ [RequirePermission(Permissions.{Module}.Delete)]
486
+ public async Task<ActionResult> Delete(Guid id, CancellationToken ct)
487
+ {
488
+ await _service.DeleteAsync(id, ct);
489
+ return NoContent();
490
+ }
491
+ }
492
+ ```
493
+
494
+ **CRITICAL — Route attribute rules:**
495
+ - `[NavRoute]` is the ONLY route attribute needed — it resolves routes dynamically from Navigation entities at startup
496
+ - **FORBIDDEN:** `[Route("api/...")]` alongside `[NavRoute]` — causes route conflicts and 404s at runtime
497
+ - **FORBIDDEN:** `[Route("api/[controller]")]` — this is standard ASP.NET Core, NOT SmartStack
498
+ - If a controller has `[NavRoute]`, there must be NO `[Route]` attribute on the class
499
+
500
+ **CRITICAL:** Use `[RequirePermission(Permissions.{Module}.{Action})]` on EVERY endpoint — NEVER `[Authorize]` alone (no RBAC enforcement).
501
+
502
+ **CRITICAL — Permission paths use IDENTICAL segments to NavRoute codes (kebab-case):**
503
+ - NavRoute: `human-resources.employees` → Permission: `human-resources.employees.read`
504
+ - NavRoute: `human-resources.employees.leaves` → Permission: `human-resources.employees.leaves.read`
505
+ - FORBIDDEN: `humanresources.employees.read` (no kebab-case — mismatches NavRoute)
506
+ - SmartStack.app convention: `support-client.my-tickets.read` (always kebab-case)
507
+
508
+ ### Section-Level Controller (NavRoute with 4 segments)
509
+
510
+ When a module has sections, each section gets its own controller with a 4-segment navRoute:
511
+
512
+ ```csharp
513
+ // Section-level controller: navRoute has 4 segments
514
+ [ApiController]
515
+ [NavRoute("{app}.{module}.{section}")]
516
+ [Authorize]
517
+ public class {Section}Controller : ControllerBase
518
+ {
519
+ // Example: human-resources.employees.departments
520
+ [HttpGet]
521
+ [RequirePermission(Permissions.{Section}.Read)]
522
+ public async Task<ActionResult<PaginatedResult<{Section}ResponseDto>>> GetAll(
523
+ [FromQuery] string? search = null,
524
+ [FromQuery] int page = 1,
525
+ [FromQuery] int pageSize = 20,
526
+ CancellationToken ct = default)
527
+ => Ok(await _service.GetAllAsync(search, page, pageSize, ct));
528
+ }
529
+ ```
530
+
531
+ **NavRoute segment rules:**
532
+ | Level | NavRoute format | Example |
533
+ |-------|----------------|---------|
534
+ | Module | `{app}.{module}` (2 segments) | `human-resources.employees` |
535
+ | Section | `{app}.{module}.{section}` (3 segments) | `human-resources.employees.departments` |
536
+
537
+ **Namespace:** `SmartStack.Api.Routing` (NOT `SmartStack.Api.Core.Routing`)
538
+
539
+ **NavRoute resolves at startup from DB:** `administration.users` → `api/administration/users`
540
+
541
+ ### Sub-Resource Pattern (NavRoute Suffix)
542
+
543
+ When an entity is a child of another entity (e.g., LeaveTypes under Leaves), use `[NavRoute(..., Suffix = "types")]`:
544
+
545
+ ```csharp
546
+ // Sub-resource controller: types are nested under leaves
547
+ [ApiController]
548
+ [NavRoute("human-resources.employees.leaves", Suffix = "types")]
549
+ [Authorize]
550
+ public class LeaveTypesController : ControllerBase
551
+ {
552
+ [HttpGet]
553
+ [RequirePermission(Permissions.Leaves.Read)] // inherits parent section permission
554
+ public async Task<ActionResult<PaginatedResult<LeaveTypeResponseDto>>> GetAll(...)
555
+ => Ok(await _service.GetAllAsync(search, page, pageSize, ct));
556
+ }
557
+ ```
558
+
559
+ **Alternative pattern** (sub-resource endpoints within parent controller):
560
+ ```csharp
561
+ // LeaveTypes as endpoints within LeavesController
562
+ [HttpGet("types")]
563
+ [RequirePermission(Permissions.Leaves.Read)]
564
+ public async Task<ActionResult<PaginatedResult<LeaveTypeResponseDto>>> GetAllLeaveTypes(...)
565
+ ```
566
+
567
+ > **CRITICAL — Sub-resource frontend completeness:**
568
+ > If a parent page has a button (e.g., "Manage Leave Types") that `navigate()`s to a sub-resource route,
569
+ > the frontend MUST include a page component for that route. Otherwise → dead link → white screen.
570
+ > - Either create a dedicated sub-resource ListPage (e.g., `LeaveTypesPage.tsx`)
571
+ > - Or DON'T include the navigate() button if pages won't be created
572
+ > - **Prefer separate controllers** (with Suffix) over sub-endpoints in parent controller — easier to route
573
+
574
+ ---
575
+
576
+ ## Navigation Seed Data Pattern (CRITICAL — routes must be full paths)
577
+
578
+ > **The navigation seed data defines menu routes stored in DB. These routes MUST be full paths starting with `/`.**
579
+ > Short routes (e.g., `humanresources`) cause 400 Bad Request on application-tracking.
580
+
581
+ ### Route Convention
582
+
583
+ | Level | Route Format | Example |
584
+ |-------|-------------|---------|
585
+ | Application | `/{app-kebab}` | `/human-resources` |
586
+ | Module | `/{app-kebab}/{module-kebab}` | `/human-resources/employees` |
587
+ | Section | `/{app-kebab}/{module-kebab}/{section-kebab}` | `/human-resources/employees/departments` |
588
+ | Resource | `/{app-kebab}/{module-kebab}/{section-kebab}/{resource-kebab}` | `/human-resources/employees/departments/export` |
589
+
590
+ **ROUTE SPECIAL CASES (list and detail sections):**
591
+ > The `list` and `detail` sections are NOT functional sub-areas — they are view modes of the module itself.
592
+ > Their navigation routes MUST NOT add extra segments:
593
+ > - `list` section route = module route (e.g., `/human-resources/employees`)
594
+ > - `detail` section route = module route + `/:id` (e.g., `/human-resources/employees/:id`)
595
+ > - FORBIDDEN: `/employees/list`, `/employees/detail/:id`
596
+ > - Other sections (dashboard, approve, import, etc.) = module route + `/{section-kebab}` (normal behavior)
597
+
598
+ **Rules:**
599
+ - Routes ALWAYS start with `/`
600
+ - Routes ALWAYS include the full hierarchy from application to current level
601
+ - Routes ALWAYS use kebab-case (NOT PascalCase, NOT camelCase)
602
+ - Code identifiers stay PascalCase in C# (`HumanResources`) but routes are kebab-case (`human-resources`)
603
+
604
+ ### ToKebabCase Helper (include in SeedConstants or SeedDataProvider)
605
+
606
+ ```csharp
607
+ private static string ToKebabCase(string value)
608
+ => System.Text.RegularExpressions.Regex
609
+ .Replace(value, "([a-z])([A-Z])", "$1-$2")
610
+ .ToLowerInvariant();
611
+ ```
612
+
613
+ ### SeedConstants Pattern
614
+
615
+ ```csharp
616
+ public static class SeedConstants
617
+ {
618
+ // Deterministic GUIDs (SHA256-based, reproducible across environments)
619
+ // NOTE: Application/Module/Section/Resource IDs are deterministic.
620
+ public static readonly Guid ApplicationId = DeterministicGuid("nav:human-resources");
621
+ public static readonly Guid ModuleId = DeterministicGuid("nav:human-resources.employees");
622
+ public static readonly Guid SectionId = DeterministicGuid("nav:human-resources.employees.departments");
623
+
624
+ private static Guid DeterministicGuid(string input)
625
+ {
626
+ var hash = System.Security.Cryptography.SHA256.HashData(
627
+ System.Text.Encoding.UTF8.GetBytes(input));
628
+ var bytes = new byte[16];
629
+ Array.Copy(hash, bytes, 16);
630
+ bytes[6] = (byte)((bytes[6] & 0x0F) | 0x50); // version 5
631
+ bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80); // variant
632
+ return new Guid(bytes);
633
+ }
634
+ }
635
+ ```
636
+
637
+ ### Navigation Seed Data Example
638
+
639
+ ```csharp
640
+ // Application: /human-resources
641
+ var app = NavigationApplication.Create(
642
+ "human-resources", "Human Resources", "HR Management",
643
+ "Users", IconType.Lucide,
644
+ "/human-resources", // FULL PATH — starts with /, kebab-case
645
+ 10);
646
+
647
+ // Module: /human-resources/employees
648
+ var module = NavigationModule.Create(
649
+ app.Id, "employees", "Employees", "Employee management",
650
+ "UserCheck", IconType.Lucide,
651
+ "/human-resources/employees", // FULL PATH — includes parent
652
+ 10);
653
+
654
+ // Section: /human-resources/employees/departments
655
+ var section = NavigationSection.Create(
656
+ module.Id, "departments", "Departments", "Manage departments",
657
+ "Building2", IconType.Lucide,
658
+ "/human-resources/employees/departments", // FULL PATH
659
+ 10);
660
+ ```
661
+
662
+ ### FORBIDDEN in Seed Data
663
+
664
+ | Mistake | Reality |
665
+ |---------|---------|
666
+ | `"humanresources"` as route | Must be `"/human-resources"` (full path, kebab-case) |
667
+ | `"employees"` as route | Must be `"/human-resources/employees"` (includes parent) |
668
+ | `Guid.NewGuid()` in seed data | Must use deterministic GUIDs (SHA256) |
669
+ | Missing translations | Must have 4 languages: fr, en, it, de |
670
+ | Missing NavigationApplicationSeedData | Menu invisible without Application level |
671
+
672
+ ---
673
+
674
+ ## DbContext Pattern (extensions)
675
+
676
+ ```csharp
677
+ // In IExtensionsDbContext.cs:
678
+ public DbSet<{Name}> {Name}s => Set<{Name}>();
679
+
680
+ // In ExtensionsDbContext.cs (same line):
681
+ public DbSet<{Name}> {Name}s => Set<{Name}>();
682
+ ```
683
+
684
+ ---
685
+
686
+ ## DI Registration Pattern
687
+
688
+ ```csharp
689
+ // In DependencyInjection.cs or ServiceCollectionExtensions.cs:
690
+ services.AddScoped<I{Name}Service, {Name}Service>();
691
+ services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
692
+ ```
693
+
694
+ ---
695
+
696
+ ## DTO Type Mapping (CRITICAL)
697
+
698
+ > **Use the correct .NET type for each property.** Incorrect types cause runtime parsing errors.
699
+
700
+ | Property Pattern | .NET Type | JSON Format | Example |
701
+ |-----------------|-----------|-------------|---------|
702
+ | `*Date`, `StartDate`, `EndDate`, `BirthDate` | `DateOnly` | `"2025-03-15"` | `public DateOnly Date { get; set; }` |
703
+ | `CreatedAt`, `UpdatedAt` | `DateTime` | `"2025-03-15T10:30:00Z"` | `public DateTime CreatedAt { get; set; }` |
704
+ | `*Time`, `StartTime` | `TimeOnly` | `"14:30:00"` | `public TimeOnly StartTime { get; set; }` |
705
+ | Duration, hours | `decimal` | `8.5` | `public decimal HoursWorked { get; set; }` |
706
+ | FK reference | `Guid` | `"uuid-string"` | `public Guid EmployeeId { get; set; }` |
707
+
708
+ **FORBIDDEN in DTOs:**
709
+ - `string Date` / `string StartDate` — use `DateOnly`
710
+ - `string Time` — use `TimeOnly`
711
+ - `DateTime BirthDate` — use `DateOnly` (no time component needed)
712
+ - `int` for hours/duration — use `decimal` for fractional values
713
+
714
+ ---
715
+
716
+ ## Common Mistakes to Avoid
717
+
718
+ | Mistake | Reality |
719
+ |---------|---------|
720
+ | `entity.SoftDelete()` | Does NOT exist — no soft delete in BaseEntity |
721
+ | `entity.Code` inherited | Code is a business property — add it yourself |
722
+ | `e.RowVersion` in config | Does NOT exist in BaseEntity |
723
+ | `e.IsDeleted` filter | Does NOT exist — no soft delete |
724
+ | `SmartStack.Api.Core.Routing` | Wrong — use `SmartStack.Api.Routing` |
725
+ | `SystemEntity` base class | Does NOT exist — use `BaseEntity` for all |
726
+ | `[Route("api/...")] + [NavRoute]` | **FORBIDDEN** — causes 404s. Only `[NavRoute]` needed (resolves route from DB at startup). Remove ALL `[Route]` attributes when `[NavRoute]` is present. |
727
+ | `SmartStack.Domain.Common.Interfaces` | Wrong — interfaces are in `SmartStack.Domain.Common` directly |
728
+ | `[Authorize]` without `[RequirePermission]` | No RBAC enforcement — always use `[RequirePermission]` |
729
+ | `tenantId: Guid.Empty` in services | OWASP A01 — always use validated `_currentTenant.TenantId` |
730
+ | Service without `ICurrentTenantService` | All tenant data leaks — inject `ICurrentTenantService` |
731
+ | `ICurrentUser` in service code | Does NOT exist — use `ICurrentUserService` + `ICurrentTenantService` |
732
+ | `_currentTenant.TenantId!.Value` | Crashes with 500 — use `?? throw new TenantContextRequiredException()` |
733
+ | `UnauthorizedAccessException("Tenant context is required")` | Returns 401 → clears frontend token. Use `TenantContextRequiredException()` (400) |
734
+ | Route `"humanresources"` in seed data | Must be full path `"/human-resources"` |
735
+ | Route without leading `/` | All routes must start with `/` |
736
+ | `humanresources.employees.read` in permissions | Permission segments MUST match NavRoute kebab-case: `human-resources.employees.read` |
737
+ | `Permission.Create()` | Does NOT exist — use `CreateForModule()`, `CreateForSection()`, etc. |
738
+ | `GetAllAsync()` without search param | ALL GetAll endpoints MUST support `?search=` for EntityLookup |
739
+ | `string Date` in DTO | Date-only fields MUST use `DateOnly`, NEVER `string` |
740
+ | `DateTime` for date-only | Use `DateOnly` when no time component needed |
741
+ | FK field as plain text input | Frontend MUST use `EntityLookup` component for Guid FK fields |
742
+ | `PagedResult<T>` / `PaginatedResultDto<T>` | FORBIDDEN — use `PaginatedResult<T>` only |
743
+
744
+ ---
745
+
746
+ ## PaginatedResult Pattern
747
+
748
+ > **Canonical type for ALL paginated responses.** One name, one contract, everywhere.
749
+
750
+ ### Definition (Backend — `SmartStack.Application.Common.Models`)
751
+
752
+ ```csharp
753
+ namespace SmartStack.Application.Common.Models;
754
+
755
+ public record PaginatedResult<T>(
756
+ List<T> Items,
757
+ int TotalCount,
758
+ int Page,
759
+ int PageSize)
760
+ {
761
+ public int TotalPages => PageSize > 0
762
+ ? (int)Math.Ceiling((double)TotalCount / PageSize) : 0;
763
+ public bool HasPreviousPage => Page > 1;
764
+ public bool HasNextPage => Page < TotalPages;
765
+
766
+ public static PaginatedResult<T> Empty(int page = 1, int pageSize = 20)
767
+ => new([], 0, page, pageSize);
768
+ }
769
+ ```
770
+
771
+ ### Extension Method
772
+
773
+ ```csharp
774
+ namespace SmartStack.Application.Common.Extensions;
775
+
776
+ public static class QueryableExtensions
777
+ {
778
+ public const int MaxPageSize = 100;
779
+
780
+ public static async Task<PaginatedResult<T>> ToPaginatedResultAsync<T>(
781
+ this IQueryable<T> query,
782
+ int page = 1,
783
+ int pageSize = 20,
784
+ CancellationToken ct = default)
785
+ {
786
+ page = Math.Max(1, page);
787
+ pageSize = Math.Clamp(pageSize, 1, MaxPageSize);
788
+
789
+ var totalCount = await query.CountAsync(ct);
790
+ var items = await query
791
+ .Skip((page - 1) * pageSize)
792
+ .Take(pageSize)
793
+ .ToListAsync(ct);
794
+
795
+ return new PaginatedResult<T>(items, totalCount, page, pageSize);
796
+ }
797
+ }
798
+ ```
799
+
800
+ ### Usage in Service (with search + extension method)
801
+
802
+ ```csharp
803
+ public async Task<PaginatedResult<{Name}ResponseDto>> GetAllAsync(
804
+ string? search = null, int page = 1, int pageSize = 20, CancellationToken ct = default)
805
+ {
806
+ var tenantId = _currentTenant.TenantId
807
+ ?? throw new TenantContextRequiredException();
808
+
809
+ var query = _db.{Name}s
810
+ .Where(x => x.TenantId == tenantId)
811
+ .AsNoTracking();
812
+
813
+ if (!string.IsNullOrWhiteSpace(search))
814
+ query = query.Where(x => x.Name.Contains(search) || x.Code.Contains(search));
815
+
816
+ return await query
817
+ .OrderBy(x => x.Name)
818
+ .Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
819
+ .ToPaginatedResultAsync(page, pageSize, ct);
820
+ }
821
+ ```
822
+
823
+ ### Frontend Types (`@/types/pagination.ts`)
824
+
825
+ ```typescript
826
+ export interface PaginatedResult<T> {
827
+ items: T[];
828
+ totalCount: number;
829
+ page: number;
830
+ pageSize: number;
831
+ totalPages: number;
832
+ hasPreviousPage: boolean;
833
+ hasNextPage: boolean;
834
+ }
835
+
836
+ export interface PaginationParams {
837
+ page?: number;
838
+ pageSize?: number;
839
+ search?: string;
840
+ sortBy?: string;
841
+ sortDirection?: 'asc' | 'desc';
842
+ }
843
+ ```
844
+
845
+ ### FORBIDDEN Type Names
846
+
847
+ | Forbidden | Canonical Replacement |
848
+ |-----------|----------------------|
849
+ | `PagedResult<T>` | `PaginatedResult<T>` |
850
+ | `PaginatedResultDto<T>` | `PaginatedResult<T>` |
851
+ | `PaginatedResponse<T>` | `PaginatedResult<T>` |
852
+ | `PageResultDto<T>` | `PaginatedResult<T>` |
853
+ | `PaginatedRequest` | `PaginationParams` |
854
+ | `QueryParameters` | `PaginationParams` |
855
+ | `currentPage` (property) | `page` |
856
+ | `HasPrevious` (property) | `HasPreviousPage` |
857
+ | `HasNext` (property) | `HasNextPage` |
858
+
859
+ ### Rules
860
+
861
+ - **Max pageSize = 100** — enforced via `Math.Clamp(pageSize, 1, 100)` or extension method
862
+ - **Default page = 1, pageSize = 20** — all GetAll endpoints
863
+ - **Search param mandatory** — enables `EntityLookup` on frontend
864
+ - **POST-CHECK 16** blocks `List<T>` returns on GetAll
865
+ - **POST-CHECK 31** blocks non-canonical pagination type names
866
+
867
+ ---
868
+
869
+ ## Critical Anti-Patterns (with code examples)
870
+
871
+ > **These are the most common and dangerous mistakes.** Each one has been observed in production code generation.
872
+
873
+ ### Anti-Pattern 1: HasQueryFilter with `!= Guid.Empty` (SECURITY — OWASP A01)
874
+
875
+ The `HasQueryFilter` in EF Core should use **runtime tenant resolution**, NOT a static comparison against `Guid.Empty`.
876
+
877
+ **INCORRECT — Does NOT isolate tenants:**
878
+ ```csharp
879
+ // WRONG: This only excludes empty GUIDs — ALL tenant data is still visible to everyone!
880
+ public void Configure(EntityTypeBuilder<MyEntity> builder)
881
+ {
882
+ builder.HasQueryFilter(e => e.TenantId != Guid.Empty);
883
+ }
884
+ ```
885
+
886
+ **CORRECT — Tenant isolation via service:**
887
+ ```csharp
888
+ // CORRECT: In SmartStack, tenant filtering is done in the SERVICE layer, not via HasQueryFilter.
889
+ public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(...)
890
+ {
891
+ var query = _db.MyEntities
892
+ .Where(x => x.TenantId == _currentUser.TenantId) // MANDATORY runtime filter
893
+ .AsNoTracking();
894
+ // ...
895
+ }
896
+ ```
897
+
898
+ **Why it's wrong:** `HasQueryFilter(e => e.TenantId != Guid.Empty)` is a **static filter** — it only removes records with empty GUIDs. It does NOT restrict data to the current tenant. This is an **OWASP A01 Broken Access Control** vulnerability.
899
+
900
+ ---
901
+
902
+ ### Anti-Pattern 2: `List<T>` instead of `PaginatedResult<T>` for GetAll
903
+
904
+ **INCORRECT — No pagination:**
905
+ ```csharp
906
+ // WRONG: Returns all records at once — no pagination, no totalCount
907
+ public async Task<List<MyEntityDto>> GetAllAsync(CancellationToken ct)
908
+ {
909
+ return await _db.MyEntities
910
+ .Where(x => x.TenantId == _currentUser.TenantId)
911
+ .Select(x => new MyEntityDto(x.Id, x.Code, x.Name))
912
+ .ToListAsync(ct);
913
+ }
914
+ ```
915
+
916
+ **CORRECT — Paginated with search:**
917
+ ```csharp
918
+ // CORRECT: Returns PaginatedResult<T> with search, page, pageSize
919
+ public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(
920
+ string? search = null, int page = 1, int pageSize = 20, CancellationToken ct = default)
921
+ {
922
+ var query = _db.MyEntities.Where(x => x.TenantId == _currentUser.TenantId).AsNoTracking();
923
+ if (!string.IsNullOrWhiteSpace(search))
924
+ query = query.Where(x => x.Name.Contains(search) || x.Code.Contains(search));
925
+ var totalCount = await query.CountAsync(ct);
926
+ var items = await query.OrderBy(x => x.Name).Skip((page - 1) * pageSize).Take(pageSize)
927
+ .Select(x => new MyEntityDto(x.Id, x.Code, x.Name, x.CreatedAt)).ToListAsync(ct);
928
+ return new PaginatedResult<MyEntityDto>(items, totalCount, page, pageSize);
929
+ }
930
+ ```
931
+
932
+ **Why it's wrong:** `List<T>` loads ALL records into memory. It also breaks `EntityLookup` which requires `{ items, totalCount }` response format.
933
+
934
+ ---
935
+
936
+ ### Anti-Pattern 3: Missing `IAuditableEntity` on tenant entities
937
+
938
+ **INCORRECT — No audit trail:**
939
+ ```csharp
940
+ // WRONG: Tenant entity without IAuditableEntity
941
+ public class MyEntity : BaseEntity, ITenantEntity
942
+ {
943
+ public Guid TenantId { get; private set; }
944
+ public string Code { get; private set; } = null!;
945
+ }
946
+ ```
947
+
948
+ **CORRECT — Always pair ITenantEntity with IAuditableEntity:**
949
+ ```csharp
950
+ public class MyEntity : BaseEntity, ITenantEntity, IAuditableEntity
951
+ {
952
+ public Guid TenantId { get; private set; }
953
+ public string? CreatedBy { get; set; }
954
+ public string? UpdatedBy { get; set; }
955
+ public string Code { get; private set; } = null!;
956
+ }
957
+ ```
958
+
959
+ **Why it's wrong:** Without `IAuditableEntity`, there is no record of who created or modified data. Mandatory for compliance in multi-tenant environments.
960
+
961
+ ---
962
+
963
+ ### Anti-Pattern 4: Code auto-generation with `Count() + 1`
964
+
965
+ **INCORRECT — Race condition:**
966
+ ```csharp
967
+ // WRONG: Two concurrent requests get the same count
968
+ var count = await _db.MyEntities.Where(x => x.TenantId == tenantId).CountAsync(ct);
969
+ return $"emp-{(count + 1):D5}";
970
+ ```
971
+
972
+ **CORRECT — Use `ICodeGenerator<T>.NextCodeAsync()` (atomic with retry):**
973
+ ```csharp
974
+ private readonly ICodeGenerator<MyEntity> _codeGenerator;
975
+
976
+ // In CreateAsync:
977
+ var code = await _codeGenerator.NextCodeAsync(ct);
978
+ var entity = MyEntity.Create(tenantId, code, dto.Name, createdBy: null);
979
+ ```
980
+
981
+ **Why it's wrong:** `Count() + 1` causes **race conditions** — concurrent requests generate duplicate codes. `ICodeGenerator<T>` uses `OrderByDescending` on existing codes + retry on unique constraint violation for safe concurrency.
982
+
983
+ **Key rules when using auto-generated codes:**
984
+ - **REMOVE** `Code` from `CreateDto` (auto-generated, not user-provided)
985
+ - **KEEP** `Code` in `ResponseDto` (returned to frontend)
986
+ - Register `ICodeGenerator<T>` in DI with `CodePatternConfig`
987
+ - Code regex in validators: `^[a-z0-9_-]+$` (supports hyphens)
988
+
989
+ **Full reference:** See `references/code-generation.md` for strategies (sequential, timestamp, yearly, UUID), volume-to-digits calculation, and complete implementation patterns.
990
+
991
+ ---
992
+
993
+ ### Anti-Pattern 5: Missing Update validator
994
+
995
+ **INCORRECT — Only CreateValidator:**
996
+ ```csharp
997
+ public class CreateMyEntityDtoValidator : AbstractValidator<CreateMyEntityDto>
998
+ {
999
+ public CreateMyEntityDtoValidator()
1000
+ {
1001
+ RuleFor(x => x.Code).NotEmpty().MaximumLength(100);
1002
+ RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
1003
+ }
1004
+ }
1005
+ // No UpdateMyEntityDtoValidator exists!
1006
+ ```
1007
+
1008
+ **CORRECT — Always create validators in pairs:**
1009
+ ```csharp
1010
+ public class CreateMyEntityDtoValidator : AbstractValidator<CreateMyEntityDto> { /* ... */ }
1011
+ public class UpdateMyEntityDtoValidator : AbstractValidator<UpdateMyEntityDto>
1012
+ {
1013
+ public UpdateMyEntityDtoValidator()
1014
+ {
1015
+ RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
1016
+ }
1017
+ }
1018
+ ```
1019
+
1020
+ **Why it's wrong:** Without an `UpdateValidator`, the Update endpoint accepts **any data without validation**.
1021
+
1022
+ ---
1023
+
1024
+ ### Anti-Pattern 6: `TenantId!.Value` null-forgiving operator (RUNTIME CRASH)
1025
+
1026
+ The `!` (null-forgiving) operator followed by `.Value` on a `Guid?` suppresses compiler warnings but **throws `InvalidOperationException` at runtime** when TenantId is null.
1027
+
1028
+ **INCORRECT — Crashes with 500 Internal Server Error:**
1029
+ ```csharp
1030
+ // WRONG: Throws InvalidOperationException("Nullable object must have a value") → 500 error
1031
+ public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(...)
1032
+ {
1033
+ var tenantId = _currentTenant.TenantId!.Value; // CRASH if no tenant context
1034
+ var query = _db.MyEntities.Where(x => x.TenantId == tenantId);
1035
+ // ...
1036
+ }
1037
+ ```
1038
+
1039
+ **CORRECT — Clean 400 via GlobalExceptionHandlerMiddleware:**
1040
+ ```csharp
1041
+ // CORRECT: Throws TenantContextRequiredException → middleware converts to 400 Bad Request
1042
+ public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(...)
1043
+ {
1044
+ var tenantId = _currentTenant.TenantId
1045
+ ?? throw new TenantContextRequiredException();
1046
+ var query = _db.MyEntities.Where(x => x.TenantId == tenantId);
1047
+ // ...
1048
+ }
1049
+ ```
1050
+
1051
+ **Why `!.Value` is wrong:** When a user hits an API via Swagger with a valid JWT but no tenant context (missing `X-Tenant-Slug` header), `TenantId` is null. The `!.Value` pattern produces an opaque `500 Internal Server Error` instead of a clear `400 Bad Request` with an actionable message.
1052
+
1053
+ **Why `UnauthorizedAccessException` is wrong:** A missing tenant is NOT an auth failure — the JWT is valid, `[Authorize]` passed. Using `UnauthorizedAccessException` returns 401, which triggers the frontend interceptor to clear the token and redirect to login. Use `TenantContextRequiredException` instead (returns 400, does not clear the token).