A showcase e-commerce platform demonstrating modern .NET microservices architecture with best practices.
Important
Since 2026, zero lines of code have been written by a human.
Before 2026, I wrote code like a normal person. Then Claude Code happened.
Now I mass-type y to approve tool permissions while Claude Code does all the actual work.
My contributions (2026–present):
- Mass-typing
y - Mass-typing
yfaster - Mass-typing
ywith increasing confidence
Job title: Senior LGTM Engineer | Chief y Officer
If it works — I mass-typed y really well. If it doesn't — I probably typed n once by accident.
| Project | Tests | Sonar |
|---|---|---|
| Backend |
Inspired by Microsoft eShop, this project showcases the latest .NET stack with a focus on:
- Modular Monolith → Microservices — Start simple, extract when needed
- DDD & CQRS — Clean domain-driven architecture with MediatR
- Event-Driven — MassTransit with transactional outbox for reliable messaging
- Cloud-Native — .NET Aspire orchestration, Kubernetes-ready
- Quick Start
- Architecture
- Tech Stack
- Project Structure
- Getting Started
- Feature Modules
- Key Patterns
- API Reference
- Testing
- Environment Variables
- Available Commands
- CI/CD
- Troubleshooting
- Kubernetes Deployment
- Dev Container
- Star History
- License
# Run with .NET Aspire (starts all services + infrastructure)
dotnet run --project src/MicroCommerce.AppHost
# Open Aspire dashboard at https://localhost:17225
# Frontend at http://localhost:3000Requirements: .NET 10 SDK, Docker Desktop, Node.js 20+
┌───────────────────────────────────────────────────────────────────┐
│ Next.js Frontend (:3000) │
│ NextAuth.js · TanStack Query · Radix UI │
└───────────────────────────┬───────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────┐
│ API Gateway (YARP) │
│ CORS · Rate Limiting · JWT Auth · X-Request-ID │
└───────────────────────────┬───────────────────────────────────────┘
│
┌───────────┬───────────┼───────────┬───────────┬───────────┐
▼ ▼ ▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ ┌────────┐
│Catalog │ │ Cart │ │ Ordering │ │Inventory │ │Profiles│ │Reviews │ ...
│ Module │ │ Module │ │ Module │ │ Module │ │ Module │ │ Module │
└───┬────┘ └───┬────┘ └────┬─────┘ └────┬─────┘ └───┬────┘ └───┬────┘
│ │ │ │ │ │
└──────────┴───────────┼────────────┴────────────┴──────────┘
▼
┌──────────────────────────┐
│ Azure Service Bus │
│ (Domain Events + Saga) │
└──────────────────────────┘
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
┌────────┐ ┌─────────────┐ ┌──────────┐
│PostgreSQL│ │ Keycloak │ │Azure Blob│
│ (appdb) │ │ (:8101) │ │ Storage │
└─────────┘ └─────────────┘ └──────────┘
Request flow: Browser → Next.js → YARP Gateway → API Service → PostgreSQL
All infrastructure runs locally via .NET Aspire with Docker containers. No separate Docker Compose needed.
| Resource | Purpose | Notes |
|---|---|---|
| PostgreSQL | Primary database (shared appdb, schema-per-module) |
Persistent volume, includes PgAdmin |
| Azure Service Bus | Domain events, saga commands | Emulator for local dev |
| Azure Blob Storage | Product images, avatars | Emulator for local dev |
| Keycloak | Identity provider (JWT + OIDC) | Port 8101, persistent volume, auto-imports realm |
| API Service | Backend API (all feature modules) | Health check at /health |
| Gateway | YARP reverse proxy | CORS, rate limiting, auth |
| Frontend | Next.js app | Port 3000 |
| Technology | Version | Purpose |
|---|---|---|
| .NET | 10 | Runtime |
| ASP.NET Core | 10 | Minimal APIs |
| .NET Aspire | 13.1.0 | Cloud-native orchestration |
| Entity Framework Core | 10 | ORM with PostgreSQL |
| MediatR | 13.1.0 | CQRS pipeline |
| MassTransit | 9.0.0 | Messaging, saga, outbox |
| FluentValidation | 12.1.1 | Request validation |
| FluentResults | 4.0.0 | Railway-oriented error handling |
| Vogen | 8.0.4 | Strongly typed IDs |
| Ardalis.SmartEnum | 8.2.0 | Domain enumerations |
| Ardalis.Specification | 9.3.1 | Query specifications |
| YARP | 2.2.0 | Reverse proxy gateway |
| SixLabors.ImageSharp | 3.1.6 | Image processing |
| Technology | Version | Purpose |
|---|---|---|
| Next.js | 16 | React framework |
| React | 19 | UI library |
| TypeScript | 5 | Type safety |
| Tailwind CSS | 4 | Styling |
| TanStack React Query | 5 | Client-side data fetching |
| Radix UI | — | Accessible component primitives |
| NextAuth.js | 5 (beta) | Authentication (Keycloak provider) |
| Recharts | — | Admin dashboard charts |
| DnD Kit | — | Drag and drop |
| Playwright | — | E2E testing |
| Biome | 2.2.0 | Linting and formatting |
| Technology | Purpose |
|---|---|
| PostgreSQL | Primary database |
| Azure Service Bus | Message broker (emulator for dev) |
| Azure Blob Storage | File storage (emulator for dev) |
| Keycloak | Identity provider |
src/
├── MicroCommerce.AppHost/ # Aspire orchestrator (entry point)
│ └── Realms/ # Keycloak realm config
├── MicroCommerce.ApiService/ # Backend API
│ ├── Features/ # Vertical slice modules
│ │ ├── Catalog/ # Products, categories, images
│ │ ├── Cart/ # Shopping cart (guest + auth)
│ │ ├── Ordering/ # Checkout, orders, saga
│ │ ├── Inventory/ # Stock management
│ │ ├── Profiles/ # User profiles, addresses, avatars
│ │ ├── Reviews/ # Product reviews (verified purchase)
│ │ ├── Wishlists/ # Authenticated user wishlists
│ │ └── Messaging/ # Dead letter queue admin UI
│ └── Common/ # Shared infrastructure
│ ├── Behaviors/ # MediatR pipeline behaviors
│ ├── Persistence/ # BaseDbContext, interceptors, conventions
│ ├── Exceptions/ # Global exception handling
│ ├── Extensions/ # FluentResults → HTTP mapping
│ ├── Messaging/ # MassTransit filters
│ └── OpenApi/ # Vogen/SmartEnum schema transformers
├── MicroCommerce.Gateway/ # YARP reverse proxy
├── MicroCommerce.ServiceDefaults/ # Aspire cross-cutting (telemetry, health)
├── MicroCommerce.ApiService.Tests/ # xUnit integration + unit tests
│ ├── Integration/ # Testcontainers + WebApplicationFactory
│ └── Unit/ # Domain logic unit tests
├── MicroCommerce.Web/ # Next.js frontend
│ ├── src/
│ │ ├── app/(storefront)/ # Customer-facing routes
│ │ ├── app/admin/ # Admin dashboard
│ │ ├── components/ # React components
│ │ ├── hooks/ # TanStack Query hooks
│ │ ├── lib/ # API client, auth, utilities
│ │ └── types/ # Type augmentations
│ └── e2e/ # Playwright E2E tests
└── BuildingBlocks/
└── BuildingBlocks.Common/ # DDD primitives (aggregates, events, value objects)
Each backend feature follows vertical slice architecture:
Features/{Name}/
{Name}Endpoints.cs # Minimal API route mapping
Domain/
Entities/ # Aggregate roots and entities
Events/ # Domain events
Application/
Commands/ # Write operations (MediatR IRequest)
Queries/ # Read operations (MediatR IRequest)
Consumers/ # MassTransit message consumers
Saga/ # MassTransit state machines (Ordering)
Specifications/ # Ardalis.Specification query specs
Infrastructure/
{Name}DbContext.cs # Owned DbContext (schema-isolated)
Configurations/ # EF Core entity configs
Migrations/ # Feature-specific EF migrations
- .NET 10 SDK
- Docker Desktop (for PostgreSQL, Keycloak, Service Bus emulator)
- Node.js 20+ (for frontend)
git clone https://github.com/baotoq/micro-commerce.git
cd micro-commerceThis single command starts all backend services, the frontend, and all infrastructure (PostgreSQL, Keycloak, Service Bus emulator, Blob Storage emulator):
dotnet run --project src/MicroCommerce.AppHostOn first run, Docker will pull container images for PostgreSQL, Keycloak, and the Azure emulators. This may take a few minutes.
| Service | URL |
|---|---|
| Aspire Dashboard | https://localhost:17225 |
| Frontend | http://localhost:3000 |
| Keycloak Admin | http://localhost:8101 |
| PgAdmin | See Aspire dashboard for port |
| OpenAPI Spec | http://localhost:5000/openapi/v1.json |
If you only want to work on the frontend (backend must be running separately):
cd src/MicroCommerce.Web
npm install
npm run devdotnet run --project src/MicroCommerce.ApiServiceNote: The backend requires PostgreSQL, Service Bus, and Keycloak to be running. Use Aspire for the full stack or start dependencies manually.
The frontend .env file is pre-configured for local development:
| Variable | Description | Default |
|---|---|---|
AUTH_SECRET |
NextAuth.js secret | Pre-set for dev |
KEYCLOAK_CLIENT_ID |
Keycloak OIDC client | nextjs-app |
KEYCLOAK_CLIENT_SECRET |
Keycloak client secret | nextjs-app-secret-change-in-production |
KEYCLOAK_ISSUER |
Keycloak realm URL | http://localhost:8101/realms/micro-commerce |
NEXT_PUBLIC_KEYCLOAK_ISSUER |
Client-side Keycloak URL | Same as above |
Aspire automatically injects services__gateway__https__0 for the frontend to discover the Gateway URL.
Products and categories with image upload, search, filtering, and status management (Draft → Published → Archived).
Domain: Product aggregate (with ProductName, Money, ProductStatus value objects), Category entity.
Events: ProductCreatedDomainEvent, ProductUpdatedDomainEvent, ProductStatusChangedDomainEvent, ProductArchivedDomainEvent
Cookie-based guest cart with authenticated cart merge on login. 30-day TTL with automatic expiration cleanup.
Domain: Cart aggregate with CartItem collection. Max 99 quantity per item. Supports AddItem, UpdateItemQuantity, RemoveItem, TransferOwnership.
Saga-based checkout orchestrating stock reservation, payment, and order confirmation across modules.
Checkout Saga flow:
CheckoutStarted→ Reserve stockStockReservationCompleted→ Wait for paymentPaymentCompleted→ Confirm order + deduct stock + clear cart- Any failure → Compensate (release reservations, mark order failed)
Domain: Order aggregate with statuses: Submitted → StockReserved → Paid → Confirmed → Shipped → Delivered (or Failed at any step).
Stock management with reservations (15-minute TTL), adjustment history, and low-stock alerts (threshold: 10 units).
Domain: StockItem aggregate with StockReservation and StockAdjustment children. Computed AvailableQuantity accounts for active reservations.
Events: StockAdjustedDomainEvent, StockLowDomainEvent, StockReservedDomainEvent, StockReleasedDomainEvent
User profiles with display names, avatar upload (max 5MB, processed via ImageSharp), and address management with default address invariant.
Domain: UserProfile aggregate with owned Address collection. Auto-created on first access.
Product reviews with verified purchase enforcement. Only users who have completed an order containing the product can leave a review.
Domain: Review aggregate with Rating (1-5) and ReviewText value objects.
Authenticated user wishlists for saving products.
Admin UI for managing dead-lettered messages from Azure Service Bus. Supports viewing, retrying, and purging DLQ messages.
Commands and queries are separate MediatR requests processed through a pipeline:
ValidationBehavior— FluentValidation rules (throws on failure)ResultValidationBehavior— FluentResults validation (returns 422 on failure)
Aggregate roots collect domain events, which are published via MassTransit after SaveChanges. The DomainEventInterceptor scans the EF Core change tracker for pending events and dispatches them through the transactional outbox.
MassTransit's EF Core outbox (OutboxDbContext in outbox schema) ensures domain events are published reliably — events are persisted in the same transaction as the domain changes, then delivered asynchronously.
All modules share a single PostgreSQL database (appdb) but are isolated into separate schemas:
| Module | Schema |
|---|---|
| Catalog | catalog |
| Cart | cart |
| Ordering | ordering |
| Inventory | inventory |
| Profiles | profiles |
| Reviews | reviews |
| Wishlists | wishlists |
| Outbox | outbox |
Each module has its own DbContext, migrations, and __EFMigrationsHistory table.
All entity IDs use Vogen value objects with UUID v7 for sortable generation:
[ValueObject<Guid>(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson)]
public partial record struct ProductId
{
public static Validation Validate(Guid value) =>
value != Guid.Empty ? Validation.Ok : Validation.Invalid("ProductId cannot be empty.");
public static ProductId New() => From(Guid.CreateVersion7());
}Interceptors (applied to all DbContexts):
AuditInterceptor— auto-setsCreatedAt/UpdatedAtonIAuditableentitiesConcurrencyInterceptor— auto-incrementsVersiononIConcurrencyTokenentitiesSoftDeleteInterceptor— converts delete to soft-delete onISoftDeletableentitiesDomainEventInterceptor— publishes domain events after SaveChanges
Model Conventions:
AuditableConvention— configures audit column typesConcurrencyTokenConvention— configures version as concurrency tokenSoftDeletableConvention— applies globalWHERE is_deleted = falsequery filter
The gateway centralizes cross-cutting concerns:
- CORS — allows
localhost:3000with credentials - Rate Limiting — sliding window: 30 req/min anonymous, 100 req/min authenticated
- JWT Auth — Keycloak JWT validation,
authenticatedpolicy for write endpoints - X-Request-ID — injected into every proxied request
- Route Authorization — read endpoints public, write endpoints require auth
MassTransit endpoints are configured with:
- Retry — exponential backoff at 1s, 5s, 25s intervals (skips
PermanentException) - Circuit breaker — 1-minute tracking window, trips at 15% failure rate, 5-minute reset
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /products |
No | List products (paginated, filterable) |
| GET | /products/{id} |
No | Get product by ID |
| POST | /products |
Gateway | Create product |
| PUT | /products/{id} |
Gateway | Update product |
| PATCH | /products/{id}/status |
Gateway | Change product status |
| DELETE | /products/{id} |
Gateway | Archive product (soft delete) |
| POST | /images |
Gateway | Upload product image |
| GET | /categories |
No | List categories |
| GET | /categories/{id} |
No | Get category |
| POST | /categories |
Gateway | Create category |
| PUT | /categories/{id} |
Gateway | Update category |
| DELETE | /categories/{id} |
Gateway | Delete category |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
No | Get cart (cookie-based identity) |
| POST | /items |
No | Add item to cart |
| PUT | /items/{itemId} |
No | Update item quantity |
| DELETE | /items/{itemId} |
No | Remove item |
| GET | /count |
No | Get item count |
| POST | /merge |
JWT | Merge guest cart into authenticated cart |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /checkout |
No | Submit order (starts saga) |
| POST | /orders/{id}/pay |
No | Simulate payment |
| GET | /orders/{id} |
No | Get order by ID |
| GET | /orders/my |
No | Get current buyer's orders |
| GET | /orders |
No | List all orders (admin) |
| GET | /dashboard |
No | Order dashboard stats (admin) |
| PATCH | /orders/{id}/status |
No | Update order status (admin) |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /stock/{productId} |
No | Get stock info |
| GET | /stock |
No | Bulk stock levels (?productIds=a,b,c) |
| POST | /stock/{productId}/adjust |
Gateway | Adjust stock |
| GET | /stock/{productId}/adjustments |
No | Adjustment history |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /me |
JWT | Get or auto-create profile |
| PUT | /me |
JWT | Update display name |
| POST | /me/avatar |
JWT | Upload avatar (max 5MB) |
| DELETE | /me/avatar |
JWT | Remove avatar |
| POST | /me/addresses |
JWT | Add address |
| PUT | /me/addresses/{id} |
JWT | Update address |
| DELETE | /me/addresses/{id} |
JWT | Delete address |
| PATCH | /me/addresses/{id}/default |
JWT | Set default address |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /products/{productId} |
No | Get product reviews |
| GET | /products/{productId}/mine |
JWT | Get user's review |
| GET | /products/{productId}/can-review |
JWT | Check eligibility |
| POST | /products/{productId} |
JWT | Create review (verified purchase) |
| PUT | /{reviewId} |
JWT | Update review |
| DELETE | /{reviewId} |
JWT | Delete review |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | / |
JWT | Get wishlist |
| GET | /count |
JWT | Get item count |
| GET | /product-ids |
JWT | Get wishlisted product IDs |
| POST | /{productId} |
JWT | Add to wishlist |
| DELETE | /{productId} |
JWT | Remove from wishlist |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /dead-letters |
JWT | List dead-lettered messages |
| POST | /dead-letters/retry |
JWT | Retry a DLQ message |
| POST | /dead-letters/purge |
JWT | Purge DLQ messages |
| Path | Description |
|---|---|
/health |
Readiness check |
/alive |
Liveness check |
/openapi/v1.json |
OpenAPI spec (dev only) |
# Run all tests (unit + integration)
dotnet test src/MicroCommerce.ApiService.Tests
# Run with coverage
dotnet test src/MicroCommerce.ApiService.Tests --collect:"XPlat Code Coverage" --settings src/MicroCommerce.ApiService.Tests/coverlet.runsettingsIntegration tests use Testcontainers to spin up a real PostgreSQL container and MassTransit's in-memory test harness. The ApiWebApplicationFactory + IntegrationTestBase provide:
- Shared PostgreSQL container across tests via
ICollectionFixture FakeAuthenticationHandlerthat injects claims fromX-Test-UserIdheaderResetDatabase()for per-test schema isolation
Test coverage: Cart, Catalog, Inventory, Ordering, Profiles, Reviews, Wishlists, Interceptors (integration); Cart, Catalog, Inventory, Ordering aggregates + validators (unit).
# Run Playwright E2E tests (requires full Aspire stack running)
cd src/MicroCommerce.Web
npx playwright test
# Run with UI mode
npx playwright test --ui
# View test report
npx playwright show-reportE2E specs: critical-path.spec.ts, product-browsing.spec.ts, user-features.spec.ts
| Variable | Required | Description |
|---|---|---|
AUTH_SECRET |
Yes | NextAuth.js encryption secret |
KEYCLOAK_CLIENT_ID |
Yes | Keycloak OIDC client ID |
KEYCLOAK_CLIENT_SECRET |
Yes | Keycloak OIDC client secret |
KEYCLOAK_ISSUER |
Yes | Keycloak realm issuer URL |
NEXT_PUBLIC_KEYCLOAK_ISSUER |
Yes | Client-side Keycloak URL |
NEXT_PUBLIC_API_URL |
No | API base URL (default: http://localhost:5200) |
services__gateway__https__0 |
Auto | Aspire-injected gateway URL |
Backend configuration is handled by Aspire service discovery and appsettings.json. Connection strings for PostgreSQL, Service Bus, Blob Storage, and Keycloak are injected automatically by Aspire.
| Command | Description |
|---|---|
dotnet run --project src/MicroCommerce.AppHost |
Start full stack via Aspire |
dotnet run --project src/MicroCommerce.ApiService |
Start backend only |
dotnet build src/ |
Build all projects |
dotnet test src/MicroCommerce.ApiService.Tests |
Run tests |
dotnet ef migrations add <Name> --context <DbContext> --output-dir Features/<Module>/Infrastructure/Migrations |
Add EF migration |
| Command | Description |
|---|---|
npm run dev |
Start dev server |
npm run build |
Production build |
npm run start |
Start production server |
npm run lint |
Run Biome linter |
npm run format |
Format with Biome |
npm run test:e2e |
Run Playwright tests |
npm run test:e2e:ui |
Playwright UI mode |
npm run test:e2e:report |
View Playwright report |
- GitHub Actions —
.github/workflows/dotnet-test.ymlruns unit + integration tests on push to master - GitHub Actions —
.github/workflows/release.ymlpublishes NuGet packages and Docker images on version tags - SonarCloud — Static analysis and code quality
- Dependabot — Weekly NuGet dependency updates
Symptom: Aspire dashboard shows services as unhealthy.
Solution: Ensure Docker Desktop is running. On first run, images need to be pulled which can take several minutes. Check Aspire dashboard logs for specific errors.
Symptom: relation "xyz" does not exist or migration-related errors.
Solution: Migrations run automatically on startup. If the database is in a bad state:
- Delete the PostgreSQL data volume: stop Aspire, run
docker volume lsto find the postgres volume, thendocker volume rm <volume-name> - Restart Aspire — it will recreate and seed the database
Symptom: Authentication fails or redirects to an error page.
Solution: Keycloak can take 30-60 seconds to start. The realm auto-imports from src/MicroCommerce.AppHost/Realms/. If the realm is corrupted, delete the Keycloak data volume and restart.
Symptom: API calls fail with network errors.
Solution:
- Verify the Gateway is running in the Aspire dashboard
- Check that
NEXT_PUBLIC_API_URLorservices__gateway__https__0is set correctly - The frontend routes through the Gateway, not directly to the API service
Symptom: Address already in use errors.
Solution: Default ports are 3000 (frontend), 8101 (Keycloak). Check for other processes using these ports:
lsof -i :3000
lsof -i :8101MIT