Full-stack .NET online book store application with event-sourced backend API and Blazor frontend, orchestrated by Aspire.
This project is a demonstration and exploration of modern .NET technologies, designed to be as complete as possible while strictly following architectural best practices, and keeping performance and scalability as core priorities.
I am sure a lot may be improved. Opening this code to the public is an opportunity to get feedback and learn from others' contributions.
A complete book store management system featuring:
- Backend API: Event-sourced ASP.NET Core Minimal APIs with Marten and PostgreSQL
- Frontend: Blazor web application for browsing and managing books
- Orchestration: Aspire for local development, deployment, and observability
- Database: PostgreSQL with event store and read model projections
- Modern Stack: .NET 10 with C# 14 (latest language features)
"A complex system that works is invariably found to have evolved from a simple system that worked. A complex system designed from scratch never works and cannot be patched up to make it work. You have to start over with a working simple system."
— John Gall
This project deliberately moves away from the "microservices-first" dogma, instead embracing a Modular Monolith approach.
There is a growing industry consensus that starting with microservices introduces accidental complexity—distributed transactions, network latency, and infrastructure overhead—before domain boundaries are strictly defined. This solution provides a concrete example of how to build a scalable, robust system without the premature complexity of a distributed mesh.
The architecture emphasizes:
-
Modularity: Loose coupling is enforced using Event Sourcing and CQRS. Features communicate via messages (Wolverine), ensuring that future decomposition into services is a seamless refactoring rather than a rewrite.
-
Pragmatism & Performance: We prioritize clean, maintainable code over academic purity. By avoiding excessive abstraction layers (like generic repositories and passthrough services), we eliminate "architectural tax," ensuring the code remains easy to refactor and runs with maximum performance.
-
Completeness: Unlike typical "Hello World" demos, this project implements production-grade requirements: resiliency, distributed tracing, structured logging, correct HTTP semantics, optimistic concurrency, hybrid caching, configuration validation, content localization, scalable real-time updates, and passwordless authentication.
-
Simplicity: By keeping the deployment unit single but the code modular, we gain the benefits of microservices (isolation, maintainability) without the operational drawbacks.
This serves as a foundational blueprint that scales with your needs, allowing you to evolve from a simple, working system into a complex one naturally.
# Prerequisites: .NET 10 SDK, Aspire CLI, Docker Desktop
# Install Aspire CLI: Follow instructions at https://aspire.dev/get-started/install-cli/
# Clone and run
# HTTPS
git clone https://github.com/aalmada/BookStore.git
# OR SSH
git clone git@github.com:aalmada/BookStore.git
cd BookStore
dotnet restore
aspire runThe Aspire dashboard opens automatically, providing access to:
- Web Frontend - Blazor application for browsing books
- API Service - Backend API with Scalar documentation at
/scalar/v1 - PostgreSQL - Event store and read model database
- PgAdmin - Database management interface
- Book Catalog with search and filtering
- Book Details with comprehensive information
- Real-time Updates with Server-Sent Events (SSE) for push notifications
- Optimistic UI for instant feedback with eventual consistency
- Responsive Design for desktop and mobile
- Type-safe API Client with BookStore.Client library (Refit-based)
- Resilience with Polly (retry and circuit breaker)
- Event Sourcing with Marten and PostgreSQL
- CQRS with async projections for optimized reads
- Real-time Notifications with Server-Sent Events (SSE) - Automatic push notifications for all mutations
- JWT Authentication - Secure token-based authentication for all clients (Web & Mobile)
- Passwordless Support - Full Passkey support including Passkey-First Sign Up (.NET 10)
- Role-Based Authorization - Admin endpoints protected
- Multi-language Support (configurable via
appsettings.json) - Full-text Search with PostgreSQL trigrams and unaccent
- Optimistic Concurrency with ETags
- Hybrid Caching - Multi-tiered caching with Redis and in-memory support
- Distributed Tracing with correlation/causation IDs
- API Versioning (header-based, v1.0)
- Soft Deletion - Logical deletes with restore capability
The project includes a custom Roslyn Analyzer (BookStore.ApiService.Analyzers) that enforces Event Sourcing, CQRS, and DDD patterns:
- ✅ Events must be immutable record types
- ✅ Commands follow CQRS conventions
- ✅ Aggregates use proper Marten Apply methods
- ✅ Handlers follow Wolverine conventions
- ✅ Consistent namespace organization
See Analyzer Rules Documentation for details.
- Native OpenAPI with Scalar UI
- Structured Logging with correlation IDs
- Service Orchestration for local development
- Service Discovery between frontend and backend
- OpenTelemetry integration for observability
- Container Management for PostgreSQL and PgAdmin
- Dashboard for monitoring all services
For a detailed breakdown of the project structure, please refer to the Getting Started Guide.
- Getting Started - Setup and first steps
- Architecture Overview - System design and patterns
- Event Sourcing Guide - Event sourcing concepts and implementation
- Aspire Orchestration Guide - Service orchestration and local development
- Marten Guide - Document DB and Event Store features
- Wolverine Integration - Command/handler pattern with Wolverine
- Configuration Guide - Options pattern and validation
- API Conventions - Time handling and JSON serialization standards
- API Client Generation - Type-safe API client with Refit
- Authentication Guide - JWT authentication and role-based authorization
- Passkey Guide - Passwordless authentication with WebAuthn/FIDO2
- Real-time Notifications - Server-Sent Events (SSE) for push notifications
- Logging Guide - Structured logging with source-generated log messages
- Correlation & Causation IDs - Distributed tracing
- Localization Guide - Multi-language support
- Caching Guide - Hybrid caching with Redis and localization support
- ETag Support - Optimistic concurrency and caching
- Performance Guide - GC optimization and performance tuning
- Testing Guide - Unit testing with TUnit, assertions, and best practices
- Integration Testing Guide - End-to-end testing with Aspire and Bogus
- Aspire Deployment Guide - Deploy to Azure and Kubernetes
- Production Scaling Guide - Scale applications and databases in production
- Contributing Guidelines - How to contribute to this project
- Blazor Web - Interactive web UI with Server rendering
- Server-Sent Events (SSE) - Real-time push notifications from server
- BookStore.Client - Reusable API client library (Refit-based)
- Polly - Resilience and transient fault handling
- ASP.NET Core 10 - Minimal APIs
- C# 14 - Latest language features (collection expressions, primary constructors, etc.)
- Marten - Event store and document DB
- Wolverine - Mediator, message bus, and async projections
- PostgreSQL 16 - Database with pg_trgm and unaccent extensions
- Aspire - Orchestration and observability
- OpenTelemetry - Distributed tracing and metrics
- Scalar - API documentation UI
- Docker - Container runtime
- TUnit - Modern testing framework with built-in code coverage
- Bogus - Fake data generation for tests
- Roslyn Analyzers - Custom analyzers for Event Sourcing/CQRS patterns
- Roslynator.Analyzers - Enhanced code analysis
- Refit - Type-safe REST library for .NET
GET /api/books- List and search books (search with?search=query)GET /api/books/{id}- Get book by ID (with ETag)GET /api/authors- List authorsGET /api/categories- List categories (localized)GET /api/publishers- List publishers
Authentication:
POST /identity/register- Register new userPOST /identity/login- Login and receive JWT access tokenPOST /identity/refresh- Refresh JWT access tokenPOST /identity/logout- Logout (invalidate token/session)
Passkey (Passwordless):
POST /account/attestation/options- Get passkey creation optionsPOST /account/attestation/result- Complete passkey registration / Sign upPOST /account/assertion/options- Get passkey login optionsPOST /account/assertion/result- Login with passkey
Account Management:
POST /identity/forgotPassword- Request password resetPOST /identity/resetPassword- Reset passwordGET /identity/manage/info- Get user informationPOST /identity/manage/info- Update user information
Note
Admin endpoints require authentication with the Admin role. Include the JWT token in the Authorization: Bearer <token> header.
POST /api/admin/books- Create bookPUT /api/admin/books/{id}- Update book (with If-Match)DELETE /api/admin/books/{id}- Soft delete bookPOST /api/admin/books/{id}/restore- Restore book- Similar CRUD for authors, categories, publishers
POST /api/admin/projections/rebuild- Rebuild projections
# Get categories in Portuguese
curl -H "Accept-Language: pt-PT" http://localhost:5000/api/categories
# Create category with translations
curl -X POST http://localhost:5000/api/admin/categories \
-H "Content-Type: application/json" \
-d '{
"name": "Software Architecture",
"translations": {
"en": {"name": "Software Architecture"},
"pt": {"name": "Arquitetura de Software"}
}
}'# All operations create events in the event store
# Create a book
curl -X POST http://localhost:5000/api/admin/books \
-H "X-Correlation-ID: workflow-123" \
-d '{"title": "Clean Code", ...}'
# → BookAdded event stored
# Update the book
curl -X PUT http://localhost:5000/api/admin/books/{id} \
-H "X-Correlation-ID: workflow-123" \
-H "If-Match: \"1\"" \
-d '{"title": "Clean Code (Updated)", ...}'
# → BookUpdated event stored
# View all events for this workflow
SELECT * FROM mt_events
WHERE correlation_id = 'workflow-123';# Get book (receives ETag)
curl -i http://localhost:5000/api/books/{id}
# ETag: "5"
# Update with concurrency check
curl -X PUT http://localhost:5000/api/admin/books/{id} \
-H "If-Match: \"5\"" \
-d '{"title": "Updated Title", ...}'
# Success → ETag: "6"
# Concurrent update fails
curl -X PUT http://localhost:5000/api/admin/books/{id} \
-H "If-Match: \"5\"" \
-d '{"title": "Another Update", ...}'
# Error: 412 Precondition Failed- Health Checks:
/health - Aspire Dashboard:
https://localhost:17161 - Scalar API Docs:
/scalar/v1 - OpenAPI Spec:
/openapi/v1.json
The project uses TUnit, a modern testing framework with built-in code coverage and parallel execution.
# Run all tests
dotnet test
# Run tests for specific project
dotnet test --project tests/ApiService/BookStore.ApiService.UnitTests/BookStore.ApiService.UnitTests.csproj
# Alternative: Run tests directly
dotnet run --project tests/ApiService/BookStore.ApiService.UnitTests/BookStore.ApiService.UnitTests.csprojNote
TUnit uses Microsoft.Testing.Platform on .NET 10+. The global.json file configures the test runner automatically.
This project is licensed under the MIT License - see the LICENSE file for details.
Copyright (c) 2025 Antao Almada
Contributions are welcome! Please read our Contributing Guidelines for details on:
- How to report issues
- How to suggest features
- Development setup and workflow
- Coding standards and best practices
- Pull request process
By contributing, you agree that your contributions will be licensed under the MIT License.