.NET Aspire distributed app (net10.0) for recipe/menu management. Backend projects live under backend/, and backend/Menu.AppHost orchestrates all services:
- MenuApi – Minimal API (Auth0 JWT-secured). Endpoints defined via
MapGroupextensions inbackend/MenuApi/Recipes/. - MenuDB – EF Core
MenuDbContext+ entity definitions (backend/MenuDB/Data/) + migrations (backend/MenuDB/Migrations/). - Menu.MigrationService – BackgroundService that applies EF migrations on startup, then exits. The API (
MenuApi) waits for this to complete before starting (WaitForCompletion). - Redis –
AddRedis("cache")resource for caching. - Menu.ServiceDefaults / Menu.ApiServiceDefaults – Shared Aspire service defaults (OpenTelemetry, health checks, Swagger).
- ui/menu-website – Vue 3 + Quasar + Vite frontend (pnpm). Connected to the API via Aspire's
AddJavaScriptApp.
Consult these before working in the relevant area:
docs/database-migrations.md— EF Core migration creation and review workflow, naming conventions, and what not to hand-edit.docs/integration-tests.md—ApiTestFixturearchitecture, Auth0 M2M test authentication, seed data, and the sequential execution model.docs/ci-path-filters.md— CI jobs selected for backend-only, frontend-only, andopen-api/-only changes.docs/specs/— architecture and design specifications for larger features.
Three distinct model layers — never mix them:
| Layer | Namespace / Location | Purpose |
|---|---|---|
| EF Entities | backend/MenuDB/Data/ (e.g. RecipeEntity) |
Database rows; configured in MenuDbContext.OnModelCreating |
| DB Models | backend/MenuApi/DBModel/ (e.g. DBModel.Recipe) |
Intermediate records using Vogen value objects; returned by repositories |
| ViewModels | backend/MenuApi/ViewModel/ (e.g. ViewModel.Recipe, NewRecipe, FullRecipe) |
API request/response DTOs |
Mapping between layers uses Riok.Mapperly (source-generated, zero-reflection) in backend/MenuApi/MappingProfiles/ViewModelMapper.cs. When adding properties, update the [MapProperty] attributes there.
Primitive types are wrapped with Vogen (backend/MenuApi/ValueObjects/). Example: RecipeId, RecipeName, IngredientAmount. Assembly-wide defaults in VogenDefaults.cs enable EF Core value converters and Swagger mapping. When creating a new value object:
[ValueObject<int>]
public readonly partial struct MyNewId { }Repositories must use .Value to unwrap and TypeName.From(x) to wrap.
# Run the full stack (API + SQL container + migrations + UI)
cd backend
dotnet run --project Menu.AppHost
# EF migrations (always from the backend solution root)
dotnet ef migrations add <Name> --project MenuDB --startup-project MenuApi
dotnet ef migrations remove --project MenuDB --startup-project MenuApi
# Unit tests
dotnet test MenuApi.Tests
# Integration tests (requires Docker for SQL Server container + Auth0 secrets)
dotnet test MenuApi.Integration.Tests- Repository-wide rule: create temporary git worktrees only under
worktrees/at the repository root. - Keep
worktrees/.gitkeepcommitted so the directory exists for automation. - When dependency update work is split into multiple independent pull request groups, create one worktree per planned branch under
worktrees/and use separate subagents to apply and validate those groups in parallel.
Agents running on Windows must substitute Bash idioms in skill commands:
| Bash | PowerShell equivalent |
|---|---|
tail -n N |
Select-Object -Last N |
grep 'pattern' |
Select-String 'pattern' |
cmd1 && cmd2 |
cmd1; if ($LASTEXITCODE -eq 0) { cmd2 } |
rm -rf <path> |
Remove-Item <path> -Recurse -Force |
cp src dst |
Copy-Item src dst |
When pushing a branch containing / from a detached HEAD worktree, always use the full refspec:
git push origin HEAD:refs/heads/<branch-name>Running a command with timeout using Start-Job: When a command may block the shell (e.g. Playwright/Chromium tests), use this pattern to run it with an explicit timeout and propagate the exit status:
$job = Start-Job { Set-Location <working-directory>; <command> }
$completed = Wait-Job $job -Timeout <timeout-seconds>
$output = Receive-Job $job
$state = $job.State
Remove-Job $job -Force
$output
if ($null -eq $completed) { throw "Command timed out after <timeout-seconds> seconds" }
if ($state -ne 'Completed') { throw "Command failed (job state: $state)" }- Unit tests (
MenuApi.Tests): xUnit + AutoFixture + FakeItEasy + AwesomeAssertions. CustomValueObjectSpecimenBuilderinCustomGenerator.csauto-constructs Vogen types via reflection; use[CustomAutoData](fromCustomAutoDataAttribute.cs) on test methods to wire it up. - Integration tests (
MenuApi.Integration.Tests): Aspire Testing spins up the full AppHost with a containerised SQL Server. All test classes must use[Collection("API Host Collection")]for sequential execution against a shared host.ShortStringAutoDataAttributelimits string length to fitvarchar(50)columns and empties collection properties. - Assertions use AwesomeAssertions (
.Should()) — not FluentAssertions.
For full-stack Playwright execution, Auth0/AppHost prerequisites, reuse and cold-start behavior, report/trace triage, and assertion/data-isolation rules, read .agents/skills/e2e-tests/SKILL.md. Run pnpm test:e2e before merging changes to ui/menu-website/src/boot/auth0.ts, ui/menu-website/src/services/auth.ts, ui/menu-website/src/services/recipe-api.ts, ui/menu-website/src/router/, AppHost wiring, or recipe CRUD endpoints. A new page-level route warrants an E2E smoke path; a new component alone does not. An unexplained failure is a diagnosis task: preserve auth, network, status, and UI assertions, and never silence it with a removed assertion, skip/fixme/only, weakened status check, or mocked real path.
TreatWarningsAsErrorsis enabled in Debug and Release for all projects.- StyleCop is configured via
backend/MenuApi/stylecop.json. ConfigureAwait(false)is used on all async calls in service/repository layers.Program.csexposes apublic partial class Programfor integration testWebApplicationFactorycompatibility.
Vue 3 SPA using Quasar component library, Vite bundler, and pnpm package manager.
- Vue 3 (Composition API) + TypeScript + Pinia (stores) + Vue Router
- Quasar v2 – UI components, SCSS variables in
src/css/quasar.variables.scss - TanStack Vue Query – server-state management with query invalidation (
src/services/recipe-service.ts) - openapi-fetch + openapi-typescript – type-safe API client generated from the backend's OpenAPI spec
- Auth0 (
@auth0/auth0-vue) – authentication; configured insrc/boot/auth0.ts - Storybook 10 – component stories co-located with components (e.g.
*.stories.ts) - Vitest (unit, jsdom) + Playwright (e2e in
e2e/)
Types and HTTP calls are generated from the backend OpenAPI spec — never hand-write API types:
# Regenerate after any API endpoint change (run from ui/menu-website)
pnpm generate-openapiThis reads open-api/menu-api.json (generated by the .NET build) and outputs src/generated/open-api/menu-api.ts. The generated types are consumed by src/services/recipe-api.ts which creates a typed openapi-fetch client with Auth0 bearer-token middleware (auth logic extracted into src/services/auth.ts).
Two-layer pattern for API interaction:
| Layer | File | Purpose |
|---|---|---|
| API layer | src/services/recipe-api.ts |
Low-level openapi-fetch calls; exports typed functions (postRecipe, getRecipes, etc.) |
| Service layer | src/services/recipe-service.ts |
Composable wrapping API calls in TanStack Query hooks (useRecipes, useCreateRecipe); handles cache invalidation |
Pages/components consume the service layer, never the API layer directly.
Routes are split into src/router/public.routes.ts (unauthenticated) and src/router/authenticated.routes.ts (protected by Auth0 authGuard). Both groups use MainLayout.vue as the parent layout.
src/components/generic/form/– reusable form fields (text-field,select-field) with co-located Storybook storiessrc/components/generic/header/– reusable header buttons (header-button) with co-located Storybook storiessrc/components/recipe/– recipe-specific components (new-recipe-form) andfields/subfolder for recipe field componentssrc/components/buttons/– auth and navigation buttons (LoginButton,LogoutButton,ProfileButton,NewRecipeHeaderButton,RecipeListButton)src/pages/– route-level page components
pnpm install # Install dependencies
pnpm dev # Dev server (standalone, port 5173)
pnpm aspire # Dev server started by Aspire (port 5173)
pnpm build # Type-check + production build
pnpm test # Vitest: unit + Storybook projects
pnpm test:unit # Vitest unit tests only (jsdom)
pnpm test:storybook # Storybook interaction tests (run after any UI change)
pnpm test:e2e # Full-stack Playwright tests; starts Aspire (Docker, SQL, migrations, API, UI)
pnpm lint # ESLint
pnpm lint-fix # ESLint with auto-fix
pnpm format # Prettier
pnpm generate-openapi # Regenerate API types from OpenAPI spec
pnpm storybook # Storybook dev server (port 6006)pnpm test:e2e requires Docker. It waits up to five minutes for the UI at
http://localhost:65276 (API http://localhost:65273) and reuses an existing stack on
that UI address. The command is long-running on Windows, so agents must use the
Start-Job timeout pattern above. The HTML report is written to
ui/menu-website/playwright-report/; failure artifacts and traces are written to
ui/menu-website/test-results/.
Authenticated e2e tests use a dedicated Auth0 test user. Set E2E_AUTH0_USERNAME and
E2E_AUTH0_PASSWORD as Windows environment variables in the shell running Playwright.
Keep non-secret Parameters__Auth0Domain and Parameters__Auth0Audience in
ui/menu-website/.env.e2e. Credentials are obtained from the repository owner;
never commit or print credentials or Playwright storageState. If credentials are
missing, ask the owner rather than replacing the real Auth0 flow with mocks or an
injected token. The account is local-only; CI must use a separate user and GitHub
Actions secrets.
Always run pnpm test:storybook after making any change under ui/menu-website/src/ — it catches regressions in components exercised by existing stories, not just changes to story files themselves.
Volta is the supported Node.js and pnpm toolchain for ui/menu-website/. Its package.json pins the project Node.js and pnpm versions. Volta's pnpm support is experimental, so set VOLTA_FEATURE_PNPM=1 in local environments before running pnpm; Volta then selects the pinned tools when commands run from the frontend directory. GitHub Actions uses volta-cli/action@v4 with the nested frontend package.json and the same feature flag. pnpm-workspace.yaml sets confirmModulesPurge: false so changing pnpm versions self-heals a stale node_modules directory automatically; if you ever see ERR_PNPM_ABORTED_REMOVE_MODULES_DIR_NO_TTY, run pnpm install once by hand from ui/menu-website/.
Every Vue component that is added or changed must come with both:
- A co-located Storybook story (
<component>.stories.ts) covering the new/changed rendering and any client-side validation, withplayinteraction assertions. - A co-located unit test (
<component>.test.ts) run by theunitVitest project (jsdom +@vue/test-utils). Mount withglobal: { plugins: [Quasar] }; addVueQueryPluginand a memoryvue-routerfor components that use them.
Split the two by what each can verify reliably:
- Stories cover rendering, props and validation messages, including MSW-mocked response bodies — register handlers per-story via the
beforeEach({ msw }) { msw.use(handler); }hook (not the legacyparameters.mswobject, whichmsw-storybook-addon's CSF-Next API silently ignores). SeeRecipeList.stories.tsandRecipeDetail.stories.tsfor examples. - Unit tests cover request payloads, success/error branches and routing, by mocking
@/services/recipe-api(the API layer) so the real service/TanStack Query wiring is still exercised. Seesrc/components/organisms/recipe/new-recipe-form.test.ts.
- ESLint flat config (
eslint.config.ts): Vue recommended + TypeScript type-checked + Vitest + Playwright + Storybook + TanStack Query plugins @typescript-eslint/consistent-type-importsenforced — useimport typefor type-only imports- Prettier for formatting (config in
.prettierrc.json), ESLint skips formatting rules via@vue/eslint-config-prettier - Path alias:
@/→src/(configured intsconfig.app.jsonandvite.config.ts) .npmrcsetsshamefully-hoist=true(required by Quasar)
- Add ViewModel DTOs in
backend/MenuApi/ViewModel/. - Add DB model records in
backend/MenuApi/DBModel/(if new data shapes are needed). - Add/update Mapperly mappings in
backend/MenuApi/MappingProfiles/ViewModelMapper.cs. - Add repository method (interface in
backend/MenuApi/Repositories/I*Repository.cs, impl in*Repository.cs). - Add service method (interface + impl in
backend/MenuApi/Services/). - Add the endpoint in the relevant
backend/MenuApi/Recipes/*Api.csfile using theMapGrouppattern. - Register new DI services in
backend/MenuApi/Program.cs.
When Codex writes to git or GitHub, it must make its authorship clear.
PR review thread comments — start the comment with this line, then a blank line, then the body:
> 🤖 **This comment was written by Codex.**
Do not add this to PR titles/descriptions, issue bodies, or issue comments.
Commit messages — end the message with a Co-authored-by: trailer naming the agent:
Co-authored-by: Codex <codex@openai.com>
Keep the subject line a normal descriptive summary — identity belongs in the trailer, not the subject, and the 🤖 comment block never goes in a commit message.
Full details in .agents/skills/agent-identity/SKILL.md.
Repo-local skills live in .agents/skills/<name>/SKILL.md. Invoke a skill with $<name> in Codex.
Issues live in GitHub Issues (gh CLI); repo is inferred from git remote -v. See docs/agents/issue-tracker.md.
Single-context layout — root CONTEXT.md + docs/adr/. See docs/agents/domain.md.