C#
CQRS Learning Project — .NET 10 + PostgreSQL
A complete, runnable learning project that teaches CQRS (Command Query Responsibility Segregation) through a realistic Order Management System built with .NET 10, PostgreSQL, and Docker Compose.
What This Project Teaches
- CQRS fundamentals — command vs query separation, read vs write models
- Clean Architecture — Domain, Application, Infrastructure, API layers
- MediatR pipeline — commands, queries, handlers, pipeline behaviors
- FluentValidation — input validation as a cross-cutting concern
- PostgreSQL — write-side schema, read-side projections, optimistic concurrency
- Docker Compose — containerized development environment
- Testing — unit tests for domain/handlers/validators, integration tests for API endpoints
CQRS Summary
CQRS separates reads (queries) from writes (commands) into distinct models:
| Write Side (Commands) | Read Side (Queries) |
|---|---|
CreateOrderCommand | GetOrderByIdQuery |
| Loads full aggregates | Projects to DTOs |
| Enforces business rules | Optimizes for consumers |
Uses IWriteDbContext | Uses IReadDbContext |
| Returns minimal data (ID) | Returns rich DTOs |
Key insight: This project uses a single PostgreSQL database for both sides. CQRS separation is logical (different interfaces, different models), not physical.
Architecture
text
┌─────────────────────────────────────────────┐
│ API Layer │
│ Command Endpoints Query Endpoints │
│ POST/PUT/DELETE GET │
├─────────────────────────────────────────────┤
│ Application Layer │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ │
│ │ Commands │ │ Queries │ │ Behaviors │ │
│ │ Handlers │ │ Handlers │ │ Logging │ │
│ │ Validators│ │ │ │ Validation│ │
│ └────┬─────┘ └────┬─────┘ └───────────┘ │
├───────┼──────────────┼───────────────────────┤
│ │ Domain │ │
│ Order Entity Value Objects │
│ OrderItem Exceptions │
├───────┼──────────────┼───────────────────────┤
│ │Infrastructure│ │
│ IWriteDbContext IReadDbContext │
│ └──────┬───────┘ │
│ AppDbContext │
│ PostgreSQL │
└─────────────────────────────────────────────┘Folder Structure
text
DotnetCQRS/
├── src/
│ ├── CqrsLearning.Api/ # HTTP endpoints, middleware, startup
│ │ ├── Endpoints/
│ │ │ ├── OrderCommandEndpoints.cs
│ │ │ └── OrderQueryEndpoints.cs
│ │ ├── Middleware/
│ │ │ └── ExceptionHandlingMiddleware.cs
│ │ └── Program.cs
│ ├── CqrsLearning.Application/ # Use cases: commands, queries, handlers
│ │ ├── Commands/
│ │ │ ├── CreateOrder/
│ │ │ ├── AddOrderItem/
│ │ │ ├── UpdateOrderStatus/
│ │ │ ├── CancelOrder/
│ │ │ └── RemoveOrderItem/
│ │ ├── Queries/
│ │ │ ├── GetOrderById/
│ │ │ ├── SearchOrders/
│ │ │ └── GetOrdersDashboard/
│ │ ├── Behaviors/
│ │ │ ├── LoggingBehavior.cs
│ │ │ └── ValidationBehavior.cs
│ │ └── Interfaces/
│ │ ├── IWriteDbContext.cs
│ │ └── IReadDbContext.cs
│ ├── CqrsLearning.Domain/ # Business entities and rules
│ │ ├── Entities/
│ │ │ ├── Order.cs
│ │ │ └── OrderItem.cs
│ │ ├── ValueObjects/
│ │ ├── Enums/
│ │ └── Exceptions/
│ ├── CqrsLearning.Infrastructure/ # EF Core, PostgreSQL, migrations
│ │ └── Persistence/
│ │ ├── AppDbContext.cs
│ │ ├── Configurations/
│ │ ├── Migrations/
│ │ └── Seed/
│ └── CqrsLearning.Contracts/ # Shared DTOs: requests and responses
│ ├── Commands/
│ ├── Queries/
│ └── Responses/
├── tests/
│ ├── CqrsLearning.UnitTests/
│ └── CqrsLearning.IntegrationTests/
├── docs/ # Documentation with Mermaid diagrams
├── docker-compose.yml
├── Dockerfile
└── CqrsLearning.slnHow to Run
Docker Compose (Recommended)
bash
docker compose up --build- API: http://localhost:8080 (Swagger UI)
- pgAdmin: http://localhost:5050 (admin@cqrs.local / admin)
Local Development
bash
# Start PostgreSQL
docker compose up postgres -d
# Run the API
cd src/CqrsLearning.Api
dotnet runAPI Endpoints
Commands (Write Side)
bash
# Create order
POST /api/orders
{
"customerName": "Jane Smith",
"customerEmail": "jane@example.com",
"shippingStreet": "100 Broadway",
"shippingCity": "New York",
"shippingState": "NY",
"shippingZipCode": "10001",
"shippingCountry": "US",
"notes": null,
"items": [
{ "productName": "Keyboard", "sku": "KB-01", "quantity": 1, "unitPrice": 149.99 }
]
}
# Add item to order
POST /api/orders/{id}/items
# Update status (Confirmed, Processing, Shipped, Delivered)
PUT /api/orders/{id}/status
{ "status": "Confirmed" }
# Cancel order
POST /api/orders/{id}/cancel
{ "reason": "Customer requested cancellation" }
# Remove item
DELETE /api/orders/{id}/items/{itemId}Queries (Read Side)
bash
# Get order with full details
GET /api/orders/{id}
# Search with filters and pagination
GET /api/orders?customerName=Jane&status=Pending&page=1&pageSize=10
# Dashboard aggregations
GET /api/orders/dashboardDatabase Setup
PostgreSQL runs in Docker. Migrations are applied automatically on startup. The database is seeded with sample orders.
To reset: docker compose down -v && docker compose up --build
Running Tests
bash
# All tests
dotnet test
# Unit tests only
dotnet test tests/CqrsLearning.UnitTests
# Integration tests only
dotnet test tests/CqrsLearning.IntegrationTestsLearning Roadmap
- Start here: Read
docs/cqrs-fundamentals.md - Understand the split: Read
docs/commands-vs-queries.md - Study the models: Read
docs/read-model-vs-write-model.md - Explore the code: Start with
Order.cs(domain) →CreateOrderCommandHandler.cs(write) →GetOrderByIdQueryHandler.cs(read) - Understand the pipeline: Read
docs/validation-and-pipelines.md - See the database: Read
docs/postgresql-design.md - Run the tests:
dotnet testand study each test file - Be critical: Read
docs/tradeoffs-and-when-not-to-use-cqrs.md - Experiment: Add a new command or query yourself
Documentation
Detailed docs with Mermaid diagrams are in the /docs folder:
| Document | Topic |
|---|---|
| Architecture | Layer diagram, dependencies |
| CQRS Fundamentals | What, why, when, misconceptions |
| Commands vs Queries | Sequence diagrams, flow comparison |
| Read vs Write Model | Entity vs DTO, multiple read models |
| PostgreSQL Design | Schema, indexes, concurrency |
| Validation & Pipelines | FluentValidation, behaviors |
| Testing Strategy | Unit, integration, test pyramid |
| Running the Project | Docker, local dev, curl examples |
| Tradeoffs | When NOT to use CQRS |
Troubleshooting
| Problem | Solution |
|---|---|
| Port 5432 in use | Stop local PostgreSQL or change port in docker-compose.yml |
| Port 8080 in use | Change ports in docker-compose.yml |
| Migration errors | docker compose down -v && docker compose up --build |
| Connection refused | Ensure PostgreSQL is running: docker compose up postgres -d |
| Tests fail | Ensure .NET 10 SDK is installed: dotnet --version |