.NET Aspire distributed app (net10.0) for recipe/menu management. Menu.AppHost orchestrates all services:
- MenuApi – Minimal API (Auth0 JWT-secured). Endpoints defined via
MapGroupextensions inMenuApi/Recipes/. - MenuDB – EF Core
MenuDbContext+ entity definitions (MenuDB/Data/) + migrations (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 | MenuDB/Data/ (e.g. RecipeEntity) |
Database rows; configured in MenuDbContext.OnModelCreating |
| DB Models | MenuApi/DBModel/ (e.g. DBModel.Recipe) |
Intermediate records using Vogen value objects; returned by repositories |
| ViewModels | MenuApi/ViewModel/ (e.g. ViewModel.Recipe, NewRecipe, FullRecipe) |
API request/response DTOs |
Mapping between layers uses Riok.Mapperly (source-generated, zero-reflection) in MenuApi/MappingProfiles/ViewModelMapper.cs. When adding properties, update the [MapProperty] attributes there.
Primitive types are wrapped with Vogen (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)
dotnet run --project Menu.AppHost
# EF migrations (always from 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- 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
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
MenuApi/ViewModel/. - Add DB model records in
MenuApi/DBModel/(if new data shapes are needed). - Add/update Mapperly mappings in
MenuApi/MappingProfiles/ViewModelMapper.cs. - Add repository method (interface in
MenuApi/Repositories/I*Repository.cs, impl in*Repository.cs). - Add service method (interface + impl in
MenuApi/Services/). - Add the endpoint in the relevant
MenuApi/Recipes/*Api.csfile using theMapGrouppattern. - Register new DI services in
MenuApi/Program.cs.