@atlashub/smartstack-cli 3.39.0 → 3.41.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 (472) 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 +6 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp-entry.mjs +6 -4
  14. package/dist/mcp-entry.mjs.map +1 -1
  15. package/package.json +115 -115
  16. package/scripts/extract-api-endpoints.ts +325 -325
  17. package/scripts/extract-business-rules.ts +440 -440
  18. package/scripts/generate-doc-with-mock-ui.ts +804 -804
  19. package/scripts/health-check.sh +168 -168
  20. package/scripts/postinstall.js +18 -18
  21. package/templates/agents/action.md +37 -37
  22. package/templates/agents/ba-reader.md +378 -378
  23. package/templates/agents/ba-writer.md +861 -861
  24. package/templates/agents/code-reviewer.md +163 -163
  25. package/templates/agents/db-reader.md +149 -149
  26. package/templates/agents/docs-context-reader.md +143 -143
  27. package/templates/agents/docs-sync-checker.md +122 -122
  28. package/templates/agents/efcore/conflicts.md +95 -84
  29. package/templates/agents/efcore/db-deploy.md +85 -74
  30. package/templates/agents/efcore/db-reset.md +96 -85
  31. package/templates/agents/efcore/db-seed.md +72 -61
  32. package/templates/agents/efcore/db-status.md +97 -86
  33. package/templates/agents/efcore/migration.md +197 -186
  34. package/templates/agents/efcore/rebase-snapshot.md +119 -108
  35. package/templates/agents/efcore/scan.md +103 -92
  36. package/templates/agents/efcore/squash.md +172 -161
  37. package/templates/agents/explore-codebase.md +66 -66
  38. package/templates/agents/explore-docs.md +98 -98
  39. package/templates/agents/fix-grammar.md +50 -50
  40. package/templates/agents/gitflow/abort.md +45 -45
  41. package/templates/agents/gitflow/cleanup.md +96 -96
  42. package/templates/agents/gitflow/commit.md +236 -236
  43. package/templates/agents/gitflow/exec.md +48 -48
  44. package/templates/agents/gitflow/finish.md +146 -146
  45. package/templates/agents/gitflow/init-clone.md +199 -199
  46. package/templates/agents/gitflow/init-detect.md +137 -137
  47. package/templates/agents/gitflow/init-validate.md +225 -225
  48. package/templates/agents/gitflow/init.md +340 -340
  49. package/templates/agents/gitflow/merge.md +145 -145
  50. package/templates/agents/gitflow/plan.md +42 -42
  51. package/templates/agents/gitflow/pr.md +191 -191
  52. package/templates/agents/gitflow/review.md +49 -49
  53. package/templates/agents/gitflow/start.md +147 -147
  54. package/templates/agents/gitflow/status.md +95 -95
  55. package/templates/agents/mcp-healthcheck.md +163 -163
  56. package/templates/agents/snipper.md +37 -37
  57. package/templates/agents/websearch.md +46 -46
  58. package/templates/hooks/appsettings-guard.sh +76 -76
  59. package/templates/hooks/docs-drift-check.md +96 -96
  60. package/templates/hooks/ef-migration-check.md +139 -139
  61. package/templates/hooks/hooks.json +58 -58
  62. package/templates/hooks/mcp-check.md +64 -64
  63. package/templates/hooks/ralph-mcp-logger.sh +46 -46
  64. package/templates/hooks/ralph-session-end.sh +69 -69
  65. package/templates/hooks/stop-hook.sh +177 -177
  66. package/templates/hooks/wsl-dotnet-cleanup.sh +24 -24
  67. package/templates/mcp-scaffolding/component.tsx.hbs +318 -318
  68. package/templates/mcp-scaffolding/controller.cs.hbs +192 -192
  69. package/templates/mcp-scaffolding/entity-extension.cs.hbs +239 -239
  70. package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +116 -116
  71. package/templates/mcp-scaffolding/frontend/nav-routes.ts.hbs +133 -133
  72. package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +126 -126
  73. package/templates/mcp-scaffolding/migrations/seed-roles.cs.hbs +261 -261
  74. package/templates/mcp-scaffolding/service-extension.cs.hbs +53 -53
  75. package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +436 -436
  76. package/templates/mcp-scaffolding/tests/entity.test.cs.hbs +239 -239
  77. package/templates/mcp-scaffolding/tests/repository.test.cs.hbs +441 -441
  78. package/templates/mcp-scaffolding/tests/security.test.cs.hbs +442 -442
  79. package/templates/mcp-scaffolding/tests/service.test.cs.hbs +402 -402
  80. package/templates/mcp-scaffolding/tests/validator.test.cs.hbs +428 -428
  81. package/templates/project/DependencyInjection.Application.cs.template +25 -25
  82. package/templates/project/DependencyInjection.Infrastructure.cs.template +61 -61
  83. package/templates/project/DesignTimeExtensionsDbContextFactory.cs.template +70 -70
  84. package/templates/project/ExampleEntity.cs.template +116 -116
  85. package/templates/project/ExampleEntityConfiguration.cs.template +64 -64
  86. package/templates/project/ExampleService.cs.template +146 -146
  87. package/templates/project/ExtensionsDbContext.cs.template +41 -41
  88. package/templates/project/IExtensionsDbContext.cs.template +22 -22
  89. package/templates/project/Program.cs.template +47 -47
  90. package/templates/project/README.md +79 -79
  91. package/templates/project/api.ts.template +12 -12
  92. package/templates/project/appsettings.json.template +170 -170
  93. package/templates/project/claude-settings.json.template +5 -5
  94. package/templates/project/test-frontend/msw/handlers.ts +58 -58
  95. package/templates/project/test-frontend/msw/server.ts +25 -25
  96. package/templates/project/test-frontend/setup.ts +16 -16
  97. package/templates/project/test-frontend/test-utils.tsx +59 -59
  98. package/templates/project/test-frontend/vitest.config.ts +31 -31
  99. package/templates/ralph/README.md +93 -93
  100. package/templates/ralph/ralph.config.yaml +113 -113
  101. package/templates/scripts/setup-ralph-loop.sh +173 -173
  102. package/templates/skills/_resources/config-safety.md +61 -61
  103. package/templates/skills/_resources/context-digest-template.md +53 -53
  104. package/templates/skills/_resources/doc-context-cache.md +60 -60
  105. package/templates/skills/_resources/docs-manifest-schema.md +155 -155
  106. package/templates/skills/_resources/formatting-guide.md +124 -124
  107. package/templates/skills/_resources/mcp-validate-documentation-spec.md +181 -181
  108. package/templates/skills/_shared.md +228 -228
  109. package/templates/skills/admin/SKILL.md +48 -48
  110. package/templates/skills/ai-prompt/SKILL.md +107 -107
  111. package/templates/skills/ai-prompt/steps/step-00-init.md +47 -47
  112. package/templates/skills/ai-prompt/steps/step-01-implementation.md +122 -122
  113. package/templates/skills/apex/SKILL.md +168 -168
  114. package/templates/skills/apex/_shared.md +141 -141
  115. package/templates/skills/apex/references/agent-teams-protocol.md +164 -164
  116. package/templates/skills/apex/references/analysis-methods.md +141 -141
  117. package/templates/skills/apex/references/challenge-questions.md +145 -145
  118. package/templates/skills/apex/references/code-generation.md +412 -412
  119. package/templates/skills/apex/references/core-seed-data.md +1437 -1437
  120. package/templates/skills/apex/references/error-classification.md +144 -144
  121. package/templates/skills/apex/references/examine-build-validation.md +82 -82
  122. package/templates/skills/apex/references/execution-frontend-gates.md +177 -177
  123. package/templates/skills/apex/references/execution-frontend-patterns.md +105 -105
  124. package/templates/skills/apex/references/execution-layer1-rules.md +96 -96
  125. package/templates/skills/apex/references/initialization-challenge-flow.md +110 -110
  126. package/templates/skills/apex/references/planning-layer-mapping.md +151 -151
  127. package/templates/skills/apex/references/post-checks.md +1584 -1584
  128. package/templates/skills/apex/references/smartstack-api.md +1053 -1053
  129. package/templates/skills/apex/references/smartstack-frontend.md +1571 -1571
  130. package/templates/skills/apex/references/smartstack-layers.md +402 -402
  131. package/templates/skills/apex/steps/step-00-init.md +307 -307
  132. package/templates/skills/apex/steps/step-01-analyze.md +165 -165
  133. package/templates/skills/apex/steps/step-02-plan.md +144 -144
  134. package/templates/skills/apex/steps/step-03-execute.md +328 -328
  135. package/templates/skills/apex/steps/step-04-examine.md +263 -263
  136. package/templates/skills/apex/steps/step-05-deep-review.md +129 -129
  137. package/templates/skills/apex/steps/step-06-resolve.md +101 -101
  138. package/templates/skills/apex/steps/step-07-tests.md +238 -238
  139. package/templates/skills/apex/steps/step-08-run-tests.md +125 -125
  140. package/templates/skills/application/SKILL.md +4 -4
  141. package/templates/skills/application/references/application-roles-template.md +227 -227
  142. package/templates/skills/application/references/backend-controller-hierarchy.md +58 -58
  143. package/templates/skills/application/references/backend-entity-seeding.md +72 -72
  144. package/templates/skills/application/references/backend-seeding-and-dto-output.md +83 -83
  145. package/templates/skills/application/references/backend-table-prefix-mapping.md +79 -79
  146. package/templates/skills/application/references/backend-verification.md +88 -88
  147. package/templates/skills/application/references/frontend-i18n-and-output.md +67 -67
  148. package/templates/skills/application/references/frontend-route-naming.md +117 -117
  149. package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +107 -107
  150. package/templates/skills/application/references/frontend-verification.md +156 -156
  151. package/templates/skills/application/references/migration-checklist-troubleshooting.md +1 -1
  152. package/templates/skills/application/references/provider-template.md +177 -177
  153. package/templates/skills/application/references/roles-client-project-handling.md +55 -55
  154. package/templates/skills/application/references/roles-fallback-procedure.md +149 -149
  155. package/templates/skills/application/references/test-coverage-requirements.md +213 -213
  156. package/templates/skills/application/references/test-frontend.md +73 -73
  157. package/templates/skills/application/references/test-prerequisites.md +72 -72
  158. package/templates/skills/application/steps/step-05-frontend.md +176 -176
  159. package/templates/skills/application/steps/step-06-migration.md +193 -193
  160. package/templates/skills/application/steps/step-07-tests.md +356 -356
  161. package/templates/skills/application/steps/step-08-documentation.md +137 -137
  162. package/templates/skills/application/templates-backend.md +463 -463
  163. package/templates/skills/application/templates-frontend.md +685 -685
  164. package/templates/skills/application/templates-i18n.md +520 -520
  165. package/templates/skills/application/templates-seed.md +1096 -1096
  166. package/templates/skills/business-analyse/SKILL.md +327 -327
  167. package/templates/skills/business-analyse/_architecture.md +123 -123
  168. package/templates/skills/business-analyse/_elicitation.md +206 -206
  169. package/templates/skills/business-analyse/_module-loop.md +115 -115
  170. package/templates/skills/business-analyse/_shared.md +383 -383
  171. package/templates/skills/business-analyse/_suggestions.md +34 -34
  172. package/templates/skills/business-analyse/html/ba-interactive.html +4477 -4477
  173. package/templates/skills/business-analyse/html/build-html.js +77 -77
  174. package/templates/skills/business-analyse/html/src/scripts/01-data-init.js +150 -150
  175. package/templates/skills/business-analyse/html/src/scripts/02-navigation.js +227 -227
  176. package/templates/skills/business-analyse/html/src/scripts/03-render-cadrage.js +199 -199
  177. package/templates/skills/business-analyse/html/src/scripts/04-render-modules.js +205 -205
  178. package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +647 -647
  179. package/templates/skills/business-analyse/html/src/scripts/06-render-consolidation.js +195 -195
  180. package/templates/skills/business-analyse/html/src/scripts/07-render-handoff.js +92 -92
  181. package/templates/skills/business-analyse/html/src/scripts/08-editing.js +135 -135
  182. package/templates/skills/business-analyse/html/src/scripts/09-export.js +168 -168
  183. package/templates/skills/business-analyse/html/src/scripts/10-comments.js +171 -171
  184. package/templates/skills/business-analyse/html/src/scripts/11-review-panel.js +166 -166
  185. package/templates/skills/business-analyse/html/src/styles/01-variables.css +38 -38
  186. package/templates/skills/business-analyse/html/src/styles/02-layout.css +101 -101
  187. package/templates/skills/business-analyse/html/src/styles/03-navigation.css +120 -120
  188. package/templates/skills/business-analyse/html/src/styles/04-cards.css +196 -196
  189. package/templates/skills/business-analyse/html/src/styles/05-modules.css +454 -454
  190. package/templates/skills/business-analyse/html/src/styles/06-wireframes.css +272 -272
  191. package/templates/skills/business-analyse/html/src/styles/07-comments.css +184 -184
  192. package/templates/skills/business-analyse/html/src/styles/08-review-panel.css +241 -241
  193. package/templates/skills/business-analyse/html/src/template.html +516 -516
  194. package/templates/skills/business-analyse/patterns/suggestion-catalog.md +546 -546
  195. package/templates/skills/business-analyse/questionnaire/00-application.md +160 -160
  196. package/templates/skills/business-analyse/questionnaire/00b-project.md +85 -85
  197. package/templates/skills/business-analyse/questionnaire/01-context.md +185 -185
  198. package/templates/skills/business-analyse/questionnaire/02-stakeholders.md +189 -189
  199. package/templates/skills/business-analyse/questionnaire/03-scope.md +164 -164
  200. package/templates/skills/business-analyse/questionnaire/04-data.md +88 -88
  201. package/templates/skills/business-analyse/questionnaire/05-integrations.md +58 -58
  202. package/templates/skills/business-analyse/questionnaire/06-security.md +68 -68
  203. package/templates/skills/business-analyse/questionnaire/07-ui.md +76 -76
  204. package/templates/skills/business-analyse/questionnaire/08-performance.md +42 -42
  205. package/templates/skills/business-analyse/questionnaire/09-constraints.md +45 -45
  206. package/templates/skills/business-analyse/questionnaire/10-documentation.md +43 -43
  207. package/templates/skills/business-analyse/questionnaire/11-data-lifecycle.md +59 -59
  208. package/templates/skills/business-analyse/questionnaire/12-migration.md +58 -58
  209. package/templates/skills/business-analyse/questionnaire/13-cross-module.md +69 -69
  210. package/templates/skills/business-analyse/questionnaire/14-risk-assumptions.md +135 -135
  211. package/templates/skills/business-analyse/questionnaire/15-success-metrics.md +136 -136
  212. package/templates/skills/business-analyse/questionnaire.md +337 -337
  213. package/templates/skills/business-analyse/react/application-viewer.md +242 -242
  214. package/templates/skills/business-analyse/react/components.md +551 -551
  215. package/templates/skills/business-analyse/react/i18n-template.md +306 -306
  216. package/templates/skills/business-analyse/references/acceptance-criteria.md +169 -169
  217. package/templates/skills/business-analyse/references/agent-module-prompt.md +362 -362
  218. package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +557 -557
  219. package/templates/skills/business-analyse/references/analysis-semantic-checks.md +190 -190
  220. package/templates/skills/business-analyse/references/cache-warming-strategy.md +566 -566
  221. package/templates/skills/business-analyse/references/cadrage-challenge-patterns.md +41 -41
  222. package/templates/skills/business-analyse/references/cadrage-coverage-matrix.md +74 -74
  223. package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +115 -115
  224. package/templates/skills/business-analyse/references/cadrage-shared-modules.md +68 -69
  225. package/templates/skills/business-analyse/references/cadrage-structure-cards.md +85 -85
  226. package/templates/skills/business-analyse/references/compilation-structure-cards.md +297 -297
  227. package/templates/skills/business-analyse/references/consolidation-structural-checks.md +107 -107
  228. package/templates/skills/business-analyse/references/deploy-data-build.md +180 -180
  229. package/templates/skills/business-analyse/references/deploy-modes.md +118 -118
  230. package/templates/skills/business-analyse/references/detection-strategies.md +424 -424
  231. package/templates/skills/business-analyse/references/entity-architecture-decision.md +218 -218
  232. package/templates/skills/business-analyse/references/handoff-file-templates.md +120 -120
  233. package/templates/skills/business-analyse/references/handoff-mappings.md +81 -81
  234. package/templates/skills/business-analyse/references/handoff-seeddata-generation.md +312 -312
  235. package/templates/skills/business-analyse/references/html-data-mapping.md +299 -299
  236. package/templates/skills/business-analyse/references/init-schema-deployment.md +65 -65
  237. package/templates/skills/business-analyse/references/naming-conventions.md +243 -243
  238. package/templates/skills/business-analyse/references/prd-generation.md +258 -258
  239. package/templates/skills/business-analyse/references/review-data-mapping.md +363 -363
  240. package/templates/skills/business-analyse/references/robustness-checks.md +542 -542
  241. package/templates/skills/business-analyse/references/spec-auto-inference.md +111 -111
  242. package/templates/skills/business-analyse/references/team-orchestration.md +1022 -1022
  243. package/templates/skills/business-analyse/references/ui-dashboard-spec.md +85 -85
  244. package/templates/skills/business-analyse/references/ui-resource-cards.md +259 -259
  245. package/templates/skills/business-analyse/references/validate-incremental-html.md +121 -121
  246. package/templates/skills/business-analyse/references/validation-checklist.md +347 -347
  247. package/templates/skills/business-analyse/references/wireframe-svg-style-guide.md +335 -335
  248. package/templates/skills/business-analyse/schemas/application-schema.json +453 -453
  249. package/templates/skills/business-analyse/schemas/feature-schema.json +53 -53
  250. package/templates/skills/business-analyse/schemas/project-schema.json +485 -485
  251. package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +201 -201
  252. package/templates/skills/business-analyse/schemas/sections/discovery-schema.json +82 -82
  253. package/templates/skills/business-analyse/schemas/sections/handoff-schema.json +80 -80
  254. package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +70 -70
  255. package/templates/skills/business-analyse/schemas/sections/specification-schema.json +547 -547
  256. package/templates/skills/business-analyse/schemas/sections/validation-schema.json +93 -93
  257. package/templates/skills/business-analyse/schemas/shared/common-defs.json +226 -226
  258. package/templates/skills/business-analyse/steps/step-00-init.md +575 -576
  259. package/templates/skills/business-analyse/steps/step-01-cadrage.md +767 -767
  260. package/templates/skills/business-analyse/steps/step-01b-applications.md +419 -419
  261. package/templates/skills/business-analyse/steps/step-02-decomposition.md +387 -387
  262. package/templates/skills/business-analyse/steps/step-03a-data.md +16 -16
  263. package/templates/skills/business-analyse/steps/step-03a1-setup.md +506 -506
  264. package/templates/skills/business-analyse/steps/step-03a2-analysis.md +252 -252
  265. package/templates/skills/business-analyse/steps/step-03b-ui.md +425 -425
  266. package/templates/skills/business-analyse/steps/step-03c-compile.md +611 -611
  267. package/templates/skills/business-analyse/steps/step-03d-validate.md +783 -783
  268. package/templates/skills/business-analyse/steps/step-04-consolidation.md +17 -17
  269. package/templates/skills/business-analyse/steps/step-04a-collect.md +415 -415
  270. package/templates/skills/business-analyse/steps/step-04b-analyze.md +163 -163
  271. package/templates/skills/business-analyse/steps/step-04c-decide.md +186 -186
  272. package/templates/skills/business-analyse/steps/step-05a-handoff.md +840 -840
  273. package/templates/skills/business-analyse/steps/step-05b-deploy.md +522 -522
  274. package/templates/skills/business-analyse/steps/step-05c-ralph-readiness.md +703 -703
  275. package/templates/skills/business-analyse/steps/step-06-review.md +278 -278
  276. package/templates/skills/business-analyse/templates/tpl-frd.md +168 -168
  277. package/templates/skills/business-analyse/templates/tpl-handoff.md +186 -186
  278. package/templates/skills/business-analyse/templates/tpl-launch-displays.md +59 -59
  279. package/templates/skills/business-analyse/templates/tpl-progress.md +172 -172
  280. package/templates/skills/business-analyse/templates-frd.md +476 -476
  281. package/templates/skills/business-analyse/templates-react.md +574 -574
  282. package/templates/skills/cc-agent/SKILL.md +129 -129
  283. package/templates/skills/cc-agent/references/agent-behavior-patterns.md +95 -95
  284. package/templates/skills/cc-agent/references/agent-frontmatter.md +213 -213
  285. package/templates/skills/cc-agent/references/permission-modes.md +102 -102
  286. package/templates/skills/cc-agent/references/tools-reference.md +144 -144
  287. package/templates/skills/cc-agent/steps/step-00-init.md +134 -134
  288. package/templates/skills/cc-agent/steps/step-01-design.md +186 -186
  289. package/templates/skills/cc-agent/steps/step-02-generate.md +131 -131
  290. package/templates/skills/cc-agent/steps/step-03-validate.md +130 -130
  291. package/templates/skills/cc-agent/templates/agent-categorized.md +67 -67
  292. package/templates/skills/cc-agent/templates/agent-standalone.md +56 -56
  293. package/templates/skills/cc-agent/templates/agent-with-skills.md +94 -94
  294. package/templates/skills/cc-audit/SKILL.md +108 -108
  295. package/templates/skills/cc-audit/references/agent-checklist.md +91 -91
  296. package/templates/skills/cc-audit/references/hook-checklist.md +110 -110
  297. package/templates/skills/cc-audit/references/skill-checklist.md +70 -70
  298. package/templates/skills/cc-audit/steps/step-00-init.md +98 -98
  299. package/templates/skills/cc-audit/steps/step-01-scan.md +142 -142
  300. package/templates/skills/cc-audit/steps/step-02-analyze.md +158 -158
  301. package/templates/skills/cc-audit/steps/step-03-report.md +142 -142
  302. package/templates/skills/cc-skill/SKILL.md +134 -134
  303. package/templates/skills/cc-skill/references/best-practices.md +167 -167
  304. package/templates/skills/cc-skill/references/frontmatter-reference.md +182 -182
  305. package/templates/skills/cc-skill/references/skill-patterns.md +199 -199
  306. package/templates/skills/cc-skill/steps/step-00-init.md +119 -119
  307. package/templates/skills/cc-skill/steps/step-01-design.md +199 -199
  308. package/templates/skills/cc-skill/steps/step-02-generate.md +145 -145
  309. package/templates/skills/cc-skill/steps/step-03-steps.md +151 -151
  310. package/templates/skills/cc-skill/steps/step-04-validate.md +124 -124
  311. package/templates/skills/cc-skill/templates/skill-forked.md +85 -85
  312. package/templates/skills/cc-skill/templates/skill-progressive.md +102 -102
  313. package/templates/skills/cc-skill/templates/skill-simple.md +75 -75
  314. package/templates/skills/cc-skill/templates/step-template.md +82 -82
  315. package/templates/skills/check-version/SKILL.md +196 -196
  316. package/templates/skills/controller/SKILL.md +162 -162
  317. package/templates/skills/controller/postman-templates.md +614 -614
  318. package/templates/skills/controller/references/controller-code-templates.md +159 -159
  319. package/templates/skills/controller/references/mcp-scaffold-workflow.md +209 -209
  320. package/templates/skills/controller/references/permission-sync-templates.md +149 -149
  321. package/templates/skills/controller/steps/step-00-init.md +193 -191
  322. package/templates/skills/controller/steps/step-01-analyze.md +146 -146
  323. package/templates/skills/controller/steps/step-02-plan.md +176 -176
  324. package/templates/skills/controller/steps/step-03-generate.md +189 -189
  325. package/templates/skills/controller/steps/step-04-perms.md +80 -80
  326. package/templates/skills/controller/steps/step-05-validate.md +107 -107
  327. package/templates/skills/controller/templates.md +1555 -1555
  328. package/templates/skills/debug/SKILL.md +70 -70
  329. package/templates/skills/debug/references/team-protocol.md +232 -232
  330. package/templates/skills/debug/steps/step-00-init.md +57 -57
  331. package/templates/skills/debug/steps/step-01-analyze.md +219 -219
  332. package/templates/skills/debug/steps/step-02-resolve.md +85 -85
  333. package/templates/skills/documentation/SKILL.md +132 -132
  334. package/templates/skills/documentation/data-schema.md +227 -227
  335. package/templates/skills/documentation/steps/step-00-init.md +70 -70
  336. package/templates/skills/documentation/steps/step-01-scan.md +113 -113
  337. package/templates/skills/documentation/steps/step-02-generate.md +231 -231
  338. package/templates/skills/documentation/steps/step-03-validate.md +251 -238
  339. package/templates/skills/documentation/templates.md +662 -663
  340. package/templates/skills/efcore/SKILL.md +168 -167
  341. package/templates/skills/efcore/references/both-contexts.md +32 -32
  342. package/templates/skills/efcore/references/database-operations.md +67 -67
  343. package/templates/skills/efcore/references/destructive-operations.md +38 -38
  344. package/templates/skills/efcore/references/reset-operations.md +81 -81
  345. package/templates/skills/efcore/references/seed-methods.md +86 -86
  346. package/templates/skills/efcore/references/shared-init-functions.md +250 -250
  347. package/templates/skills/efcore/references/sql-objects-injection.md +61 -61
  348. package/templates/skills/efcore/references/troubleshooting.md +81 -81
  349. package/templates/skills/efcore/references/zero-downtime-patterns.md +227 -227
  350. package/templates/skills/efcore/steps/db/step-deploy.md +217 -217
  351. package/templates/skills/efcore/steps/db/step-reset.md +186 -186
  352. package/templates/skills/efcore/steps/db/step-seed.md +166 -166
  353. package/templates/skills/efcore/steps/db/step-status.md +173 -173
  354. package/templates/skills/efcore/steps/migration/step-00-init.md +102 -102
  355. package/templates/skills/efcore/steps/migration/step-01-check.md +164 -164
  356. package/templates/skills/efcore/steps/migration/step-02-create.md +160 -160
  357. package/templates/skills/efcore/steps/migration/step-03-validate.md +168 -168
  358. package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +173 -173
  359. package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +100 -100
  360. package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +115 -115
  361. package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +112 -112
  362. package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +157 -157
  363. package/templates/skills/efcore/steps/shared/step-00-init.md +131 -131
  364. package/templates/skills/efcore/steps/squash/step-00-init.md +141 -141
  365. package/templates/skills/efcore/steps/squash/step-01-backup.md +120 -120
  366. package/templates/skills/efcore/steps/squash/step-02-fetch.md +168 -168
  367. package/templates/skills/efcore/steps/squash/step-03-create.md +184 -184
  368. package/templates/skills/efcore/steps/squash/step-04-validate.md +174 -174
  369. package/templates/skills/explore/SKILL.md +98 -98
  370. package/templates/skills/feature-full/SKILL.md +111 -111
  371. package/templates/skills/feature-full/steps/step-00-init.md +57 -57
  372. package/templates/skills/feature-full/steps/step-01-implementation.md +120 -120
  373. package/templates/skills/gitflow/SKILL.md +377 -377
  374. package/templates/skills/gitflow/_shared.md +620 -620
  375. package/templates/skills/gitflow/phases/abort.md +189 -189
  376. package/templates/skills/gitflow/phases/cleanup.md +234 -234
  377. package/templates/skills/gitflow/phases/status.md +192 -192
  378. package/templates/skills/gitflow/references/commit-message-generation.md +58 -58
  379. package/templates/skills/gitflow/references/commit-migration-validation.md +49 -49
  380. package/templates/skills/gitflow/references/finish-cleanup.md +55 -55
  381. package/templates/skills/gitflow/references/finish-version-bumping.md +45 -45
  382. package/templates/skills/gitflow/references/init-config-template.md +135 -135
  383. package/templates/skills/gitflow/references/init-environment-detection.md +41 -41
  384. package/templates/skills/gitflow/references/init-name-normalization.md +103 -103
  385. package/templates/skills/gitflow/references/init-questions.md +185 -185
  386. package/templates/skills/gitflow/references/init-structure-creation.md +75 -75
  387. package/templates/skills/gitflow/references/init-version-detection.md +21 -21
  388. package/templates/skills/gitflow/references/init-workspace-detection.md +43 -43
  389. package/templates/skills/gitflow/references/merge-ci-status.md +36 -36
  390. package/templates/skills/gitflow/references/merge-execution.md +62 -62
  391. package/templates/skills/gitflow/references/merge-pr-context.md +76 -76
  392. package/templates/skills/gitflow/references/plan-template.md +69 -69
  393. package/templates/skills/gitflow/references/pr-build-checks.md +60 -60
  394. package/templates/skills/gitflow/references/pr-generation.md +58 -58
  395. package/templates/skills/gitflow/references/start-branch-normalization.md +28 -28
  396. package/templates/skills/gitflow/references/start-efcore-preflight.md +70 -70
  397. package/templates/skills/gitflow/references/start-local-config.md +113 -113
  398. package/templates/skills/gitflow/references/start-worktree-creation.md +50 -50
  399. package/templates/skills/gitflow/references/sync-push-verify.md +44 -44
  400. package/templates/skills/gitflow/references/sync-rebase-conflicts.md +38 -38
  401. package/templates/skills/gitflow/steps/step-commit.md +199 -199
  402. package/templates/skills/gitflow/steps/step-finish.md +147 -147
  403. package/templates/skills/gitflow/steps/step-init.md +190 -190
  404. package/templates/skills/gitflow/steps/step-merge.md +85 -85
  405. package/templates/skills/gitflow/steps/step-plan.md +151 -151
  406. package/templates/skills/gitflow/steps/step-pr.md +199 -199
  407. package/templates/skills/gitflow/steps/step-start.md +195 -195
  408. package/templates/skills/gitflow/steps/step-sync.md +161 -161
  409. package/templates/skills/gitflow/templates/config.json +72 -72
  410. package/templates/skills/mcp/SKILL.md +62 -62
  411. package/templates/skills/mcp/steps/step-01-healthcheck.md +108 -108
  412. package/templates/skills/mcp/steps/step-02-tools.md +73 -73
  413. package/templates/skills/notification/SKILL.md +173 -173
  414. package/templates/skills/quick-search/SKILL.md +99 -99
  415. package/templates/skills/ralph-loop/SKILL.md +234 -234
  416. package/templates/skills/ralph-loop/references/category-completeness.md +185 -185
  417. package/templates/skills/ralph-loop/references/category-rules.md +96 -96
  418. package/templates/skills/ralph-loop/references/compact-loop.md +300 -300
  419. package/templates/skills/ralph-loop/references/init-resume-recovery.md +127 -127
  420. package/templates/skills/ralph-loop/references/module-transition.md +151 -151
  421. package/templates/skills/ralph-loop/references/multi-module-queue.md +171 -171
  422. package/templates/skills/ralph-loop/references/parallel-execution.md +246 -246
  423. package/templates/skills/ralph-loop/references/section-splitting.md +439 -439
  424. package/templates/skills/ralph-loop/references/task-transform-legacy.md +256 -256
  425. package/templates/skills/ralph-loop/references/team-orchestration.md +547 -547
  426. package/templates/skills/ralph-loop/steps/step-00-init.md +150 -150
  427. package/templates/skills/ralph-loop/steps/step-01-task.md +174 -174
  428. package/templates/skills/ralph-loop/steps/step-02-execute.md +177 -177
  429. package/templates/skills/ralph-loop/steps/step-03-commit.md +92 -92
  430. package/templates/skills/ralph-loop/steps/step-04-check.md +207 -207
  431. package/templates/skills/ralph-loop/steps/step-05-report.md +175 -175
  432. package/templates/skills/refactor/SKILL.md +56 -56
  433. package/templates/skills/refactor/steps/step-01-discover.md +60 -60
  434. package/templates/skills/refactor/steps/step-02-execute.md +67 -67
  435. package/templates/skills/review-code/SKILL.md +95 -94
  436. package/templates/skills/review-code/references/clean-code-principles.md +292 -292
  437. package/templates/skills/review-code/references/code-quality-metrics.md +174 -174
  438. package/templates/skills/review-code/references/feedback-patterns.md +149 -149
  439. package/templates/skills/review-code/references/owasp-api-top10.md +243 -243
  440. package/templates/skills/review-code/references/security-checklist.md +212 -212
  441. package/templates/skills/review-code/steps/step-01-smartstack.md +96 -96
  442. package/templates/skills/review-code/steps/step-02-detailed-review.md +80 -80
  443. package/templates/skills/review-code/steps/step-03-react.md +44 -44
  444. package/templates/skills/ui-components/SKILL.md +137 -137
  445. package/templates/skills/ui-components/accessibility.md +170 -170
  446. package/templates/skills/ui-components/patterns/dashboard-chart.md +327 -327
  447. package/templates/skills/ui-components/patterns/data-table.md +39 -39
  448. package/templates/skills/ui-components/patterns/entity-card.md +77 -77
  449. package/templates/skills/ui-components/patterns/grid-layout.md +91 -91
  450. package/templates/skills/ui-components/patterns/kanban.md +43 -43
  451. package/templates/skills/ui-components/responsive-guidelines.md +278 -278
  452. package/templates/skills/ui-components/style-guide.md +113 -113
  453. package/templates/skills/utils/SKILL.md +44 -44
  454. package/templates/skills/utils/subcommands/test-web-config.md +152 -152
  455. package/templates/skills/utils/subcommands/test-web.md +123 -123
  456. package/templates/skills/validate/SKILL.md +181 -181
  457. package/templates/skills/validate-feature/SKILL.md +101 -101
  458. package/templates/skills/validate-feature/references/api-smoke-tests.md +140 -140
  459. package/templates/skills/validate-feature/references/db-validation-checks.md +180 -180
  460. package/templates/skills/validate-feature/steps/step-00-dependencies.md +121 -121
  461. package/templates/skills/validate-feature/steps/step-01-compile.md +39 -39
  462. package/templates/skills/validate-feature/steps/step-02-unit-tests.md +45 -45
  463. package/templates/skills/validate-feature/steps/step-03-integration-tests.md +53 -53
  464. package/templates/skills/validate-feature/steps/step-04-api-smoke.md +94 -94
  465. package/templates/skills/validate-feature/steps/step-05-db-validation.md +149 -149
  466. package/templates/skills/workflow/SKILL.md +127 -127
  467. package/templates/skills/workflow/steps/step-00-init.md +57 -57
  468. package/templates/skills/workflow/steps/step-01-implementation.md +84 -84
  469. package/templates/test-web/api-health.json +38 -38
  470. package/templates/test-web/minimal.json +19 -19
  471. package/templates/test-web/npm-package.json +46 -46
  472. 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).