MicroCommerce

February 26, 2026 · View on GitHub

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 y faster
  • Mass-typing y with 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.

ProjectTestsSonar
BackendTest ResultQuality Gate Status Lines of Code

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

Table of Contents

Quick Start

# 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:3000

Requirements: .NET 10 SDK, Docker Desktop, Node.js 20+

Architecture

┌───────────────────────────────────────────────────────────────────┐
│                       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.

Aspire Topology

ResourcePurposeNotes
PostgreSQLPrimary database (shared appdb, schema-per-module)Persistent volume, includes PgAdmin
Azure Service BusDomain events, saga commandsEmulator for local dev
Azure Blob StorageProduct images, avatarsEmulator for local dev
KeycloakIdentity provider (JWT + OIDC)Port 8101, persistent volume, auto-imports realm
API ServiceBackend API (all feature modules)Health check at /health
GatewayYARP reverse proxyCORS, rate limiting, auth
FrontendNext.js appPort 3000

Tech Stack

Backend

TechnologyVersionPurpose
.NET10Runtime
ASP.NET Core10Minimal APIs
.NET Aspire13.1.0Cloud-native orchestration
Entity Framework Core10ORM with PostgreSQL
MediatR13.1.0CQRS pipeline
MassTransit9.0.0Messaging, saga, outbox
FluentValidation12.1.1Request validation
FluentResults4.0.0Railway-oriented error handling
Vogen8.0.4Strongly typed IDs
Ardalis.SmartEnum8.2.0Domain enumerations
Ardalis.Specification9.3.1Query specifications
YARP2.2.0Reverse proxy gateway
SixLabors.ImageSharp3.1.6Image processing

Frontend

TechnologyVersionPurpose
Next.js16React framework
React19UI library
TypeScript5Type safety
Tailwind CSS4Styling
TanStack React Query5Client-side data fetching
Radix UIAccessible component primitives
NextAuth.js5 (beta)Authentication (Keycloak provider)
RechartsAdmin dashboard charts
DnD KitDrag and drop
PlaywrightE2E testing
Biome2.2.0Linting and formatting

Infrastructure

TechnologyPurpose
PostgreSQLPrimary database
Azure Service BusMessage broker (emulator for dev)
Azure Blob StorageFile storage (emulator for dev)
KeycloakIdentity provider

Project Structure

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)

Feature Module Structure

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

Getting Started

Prerequisites

1. Clone the Repository

git clone https://github.com/baotoq/micro-commerce.git
cd micro-commerce

2. Run with Aspire

This single command starts all backend services, the frontend, and all infrastructure (PostgreSQL, Keycloak, Service Bus emulator, Blob Storage emulator):

dotnet run --project src/MicroCommerce.AppHost

On first run, Docker will pull container images for PostgreSQL, Keycloak, and the Azure emulators. This may take a few minutes.

3. Access the Application

ServiceURL
Aspire Dashboardhttps://localhost:17225
Frontendhttp://localhost:3000
Keycloak Adminhttp://localhost:8101
PgAdminSee Aspire dashboard for port
OpenAPI Spechttp://localhost:5000/openapi/v1.json

4. Frontend-Only Development

If you only want to work on the frontend (backend must be running separately):

cd src/MicroCommerce.Web
npm install
npm run dev

5. Backend-Only Development

dotnet run --project src/MicroCommerce.ApiService

Note: The backend requires PostgreSQL, Service Bus, and Keycloak to be running. Use Aspire for the full stack or start dependencies manually.

6. Environment Setup

The frontend .env file is pre-configured for local development:

VariableDescriptionDefault
AUTH_SECRETNextAuth.js secretPre-set for dev
KEYCLOAK_CLIENT_IDKeycloak OIDC clientnextjs-app
KEYCLOAK_CLIENT_SECRETKeycloak client secretnextjs-app-secret-change-in-production
KEYCLOAK_ISSUERKeycloak realm URLhttp://localhost:8101/realms/micro-commerce
NEXT_PUBLIC_KEYCLOAK_ISSUERClient-side Keycloak URLSame as above

Aspire automatically injects services__gateway__https__0 for the frontend to discover the Gateway URL.

Feature Modules

Catalog

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

Cart

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.

Ordering

Saga-based checkout orchestrating stock reservation, payment, and order confirmation across modules.

Checkout Saga flow:

  1. CheckoutStarted → Reserve stock
  2. StockReservationCompleted → Wait for payment
  3. PaymentCompleted → Confirm order + deduct stock + clear cart
  4. Any failure → Compensate (release reservations, mark order failed)

Domain: Order aggregate with statuses: Submitted → StockReserved → Paid → Confirmed → Shipped → Delivered (or Failed at any step).

Inventory

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

Profiles

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.

Reviews

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.

Wishlists

Authenticated user wishlists for saving products.

Messaging (DLQ)

Admin UI for managing dead-lettered messages from Azure Service Bus. Supports viewing, retrying, and purging DLQ messages.

Key Patterns

CQRS with MediatR

Commands and queries are separate MediatR requests processed through a pipeline:

  1. ValidationBehavior — FluentValidation rules (throws on failure)
  2. ResultValidationBehavior — FluentResults validation (returns 422 on failure)

Domain Events

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.

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.

Schema-per-Feature Database

All modules share a single PostgreSQL database (appdb) but are isolated into separate schemas:

ModuleSchema
Catalogcatalog
Cartcart
Orderingordering
Inventoryinventory
Profilesprofiles
Reviewsreviews
Wishlistswishlists
Outboxoutbox

Each module has its own DbContext, migrations, and __EFMigrationsHistory table.

Strongly Typed IDs (Vogen)

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());
}

EF Core Interceptors & Conventions

Interceptors (applied to all DbContexts):

  • AuditInterceptor — auto-sets CreatedAt/UpdatedAt on IAuditable entities
  • ConcurrencyInterceptor — auto-increments Version on IConcurrencyToken entities
  • SoftDeleteInterceptor — converts delete to soft-delete on ISoftDeletable entities
  • DomainEventInterceptor — publishes domain events after SaveChanges

Model Conventions:

  • AuditableConvention — configures audit column types
  • ConcurrencyTokenConvention — configures version as concurrency token
  • SoftDeletableConvention — applies global WHERE is_deleted = false query filter

YARP Gateway

The gateway centralizes cross-cutting concerns:

  • CORS — allows localhost:3000 with credentials
  • Rate Limiting — sliding window: 30 req/min anonymous, 100 req/min authenticated
  • JWT Auth — Keycloak JWT validation, authenticated policy for write endpoints
  • X-Request-ID — injected into every proxied request
  • Route Authorization — read endpoints public, write endpoints require auth

Resilience

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

API Reference

Catalog (/api/catalog)

MethodPathAuthDescription
GET/productsNoList products (paginated, filterable)
GET/products/{id}NoGet product by ID
POST/productsGatewayCreate product
PUT/products/{id}GatewayUpdate product
PATCH/products/{id}/statusGatewayChange product status
DELETE/products/{id}GatewayArchive product (soft delete)
POST/imagesGatewayUpload product image
GET/categoriesNoList categories
GET/categories/{id}NoGet category
POST/categoriesGatewayCreate category
PUT/categories/{id}GatewayUpdate category
DELETE/categories/{id}GatewayDelete category

Cart (/api/cart)

MethodPathAuthDescription
GET/NoGet cart (cookie-based identity)
POST/itemsNoAdd item to cart
PUT/items/{itemId}NoUpdate item quantity
DELETE/items/{itemId}NoRemove item
GET/countNoGet item count
POST/mergeJWTMerge guest cart into authenticated cart

Ordering (/api/ordering)

MethodPathAuthDescription
POST/checkoutNoSubmit order (starts saga)
POST/orders/{id}/payNoSimulate payment
GET/orders/{id}NoGet order by ID
GET/orders/myNoGet current buyer's orders
GET/ordersNoList all orders (admin)
GET/dashboardNoOrder dashboard stats (admin)
PATCH/orders/{id}/statusNoUpdate order status (admin)

Inventory (/api/inventory)

MethodPathAuthDescription
GET/stock/{productId}NoGet stock info
GET/stockNoBulk stock levels (?productIds=a,b,c)
POST/stock/{productId}/adjustGatewayAdjust stock
GET/stock/{productId}/adjustmentsNoAdjustment history

Profiles (/api/profiles)

MethodPathAuthDescription
GET/meJWTGet or auto-create profile
PUT/meJWTUpdate display name
POST/me/avatarJWTUpload avatar (max 5MB)
DELETE/me/avatarJWTRemove avatar
POST/me/addressesJWTAdd address
PUT/me/addresses/{id}JWTUpdate address
DELETE/me/addresses/{id}JWTDelete address
PATCH/me/addresses/{id}/defaultJWTSet default address

Reviews (/api/reviews)

MethodPathAuthDescription
GET/products/{productId}NoGet product reviews
GET/products/{productId}/mineJWTGet user's review
GET/products/{productId}/can-reviewJWTCheck eligibility
POST/products/{productId}JWTCreate review (verified purchase)
PUT/{reviewId}JWTUpdate review
DELETE/{reviewId}JWTDelete review

Wishlists (/api/wishlist)

MethodPathAuthDescription
GET/JWTGet wishlist
GET/countJWTGet item count
GET/product-idsJWTGet wishlisted product IDs
POST/{productId}JWTAdd to wishlist
DELETE/{productId}JWTRemove from wishlist

Messaging (/api/messaging)

MethodPathAuthDescription
GET/dead-lettersJWTList dead-lettered messages
POST/dead-letters/retryJWTRetry a DLQ message
POST/dead-letters/purgeJWTPurge DLQ messages

System Endpoints

PathDescription
/healthReadiness check
/aliveLiveness check
/openapi/v1.jsonOpenAPI spec (dev only)

Testing

Backend Tests

# 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.runsettings

Integration 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
  • FakeAuthenticationHandler that injects claims from X-Test-UserId header
  • ResetDatabase() for per-test schema isolation

Test coverage: Cart, Catalog, Inventory, Ordering, Profiles, Reviews, Wishlists, Interceptors (integration); Cart, Catalog, Inventory, Ordering aggregates + validators (unit).

E2E Tests

# 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-report

E2E specs: critical-path.spec.ts, product-browsing.spec.ts, user-features.spec.ts

Environment Variables

Frontend (src/MicroCommerce.Web/.env)

VariableRequiredDescription
AUTH_SECRETYesNextAuth.js encryption secret
KEYCLOAK_CLIENT_IDYesKeycloak OIDC client ID
KEYCLOAK_CLIENT_SECRETYesKeycloak OIDC client secret
KEYCLOAK_ISSUERYesKeycloak realm issuer URL
NEXT_PUBLIC_KEYCLOAK_ISSUERYesClient-side Keycloak URL
NEXT_PUBLIC_API_URLNoAPI base URL (default: http://localhost:5200)
services__gateway__https__0AutoAspire-injected gateway URL

Backend

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.

Available Commands

Backend

CommandDescription
dotnet run --project src/MicroCommerce.AppHostStart full stack via Aspire
dotnet run --project src/MicroCommerce.ApiServiceStart backend only
dotnet build src/Build all projects
dotnet test src/MicroCommerce.ApiService.TestsRun tests
dotnet ef migrations add <Name> --context <DbContext> --output-dir Features/<Module>/Infrastructure/MigrationsAdd EF migration

Frontend

CommandDescription
npm run devStart dev server
npm run buildProduction build
npm run startStart production server
npm run lintRun Biome linter
npm run formatFormat with Biome
npm run test:e2eRun Playwright tests
npm run test:e2e:uiPlaywright UI mode
npm run test:e2e:reportView Playwright report

CI/CD

  • GitHub Actions.github/workflows/dotnet-test.yml runs unit + integration tests on push to master
  • GitHub Actions.github/workflows/release.yml publishes NuGet packages and Docker images on version tags
  • SonarCloud — Static analysis and code quality
  • Dependabot — Weekly NuGet dependency updates

Troubleshooting

Docker Containers Not Starting

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.

Database Migration 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:

  1. Delete the PostgreSQL data volume: stop Aspire, run docker volume ls to find the postgres volume, then docker volume rm <volume-name>
  2. Restart Aspire — it will recreate and seed the database

Keycloak Not Ready

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.

Frontend Can't Reach Backend

Symptom: API calls fail with network errors.

Solution:

  1. Verify the Gateway is running in the Aspire dashboard
  2. Check that NEXT_PUBLIC_API_URL or services__gateway__https__0 is set correctly
  3. The frontend routes through the Gateway, not directly to the API service

Port Conflicts

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 :8101

Star History

Star History Chart

License

MIT