.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.
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.
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 tests
pnpm test:e2e # Playwright end-to-end tests
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)- 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.