An educational REST API demonstrating Domain-Driven Design (DDD) patterns, Clean Architecture, and .NET best practices using .NET 10 and .NET Aspire.
- Overview
- Architecture
- Key Patterns Demonstrated
- Technology Stack
- Getting Started
- Project Structure
- API Endpoints
- Learning Resources
This project is an educational showcase of DDD tactical patterns, Clean Architecture principles, and modern .NET development practices. It implements a simplified order management system - complex enough to demonstrate key concepts, simple enough to understand quickly.
- β DDD Tactical Patterns: Aggregates, Entities, Value Objects, Domain Services, Repository Pattern, Domain Events
- β Clean Architecture: Strict layer separation with proper dependency flow
- β Persistence Ignorance: Domain models completely separated from EF Core entities
- β CQRS Lite: Commands for writes, Queries for reads using MediatR
- β Minimal APIs: Modern ASP.NET Core endpoints with MapGroup and TypedResults
- β .NET Aspire: Cloud-native orchestration, observability, and developer experience
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Presentation Layer β
β (DDDPlayground.ApiService) β
β Minimal APIs β’ OpenAPI β’ HTTP Endpoints β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β depends on
ββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ
β Application Layer β
β (DDDPlayground.Application) β
β CQRS Handlers β’ DTOs β’ Use Case Orchestration β
ββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββ
β depends on
ββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββ
β Domain Layer β
β (DDDPlayground.Domain) β
β Aggregates β’ Entities β’ Value Objects β’ Services β
β (Zero Infrastructure Dependencies) β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β²
β implements interfaces
ββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββ
β Infrastructure Layer β
β (DDDPlayground.Infrastructure) β
β EF Core β’ PostgreSQL β’ Repositories β’ Persistence Modelsβ
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
This project demonstrates true domain/persistence separation with three distinct model layers:
-
Domain Models (
DDDPlayground.Domain)- Pure business logic, zero persistence attributes
- Rich domain model with encapsulation
- Example:
Orderaggregate with private setters and business methods
-
Persistence Models (
DDDPlayground.Infrastructure/Persistence/Models)- Separate EF Core entities (e.g.,
OrderEntity,OrderItemEntity) - Optimized for database storage
- Different class hierarchy from domain
- Separate EF Core entities (e.g.,
-
API Models (
DDDPlayground.ApiService/Models)- Request/Response DTOs
- Optimized for HTTP communication
- Prevent over-posting
Manual Mappers in DDDPlayground.Infrastructure/Persistence/Mappers handle explicit transformation between layers.
- Aggregate Root:
Ordercontrols access toOrderItementities - Value Objects:
Money,OrderId,CustomerIdwith immutability and value equality - Domain Services:
PricingServicefor discount calculation logic - Domain Events:
OrderConfirmedEventfor in-memory event dispatching - Repository Pattern:
IOrderRepositoryinterface in domain, implementation in infrastructure
- Dependency Inversion: Domain layer has zero dependencies
- Persistence Ignorance: No EF Core attributes in domain models
- Manual Mapping: Explicit
ToDomain()andToEntity()methods (no AutoMapper) - Use Case Orchestration: MediatR handlers coordinate workflows
- Minimal APIs:
MapGroup(),TypedResults, static handlers for performance - CQRS Lite: Separate commands (
CreateOrderCommand) and queries (GetOrderQuery) - Unit of Work: Transaction boundaries per use case
- .NET Aspire: Orchestration, PostgreSQL hosting, observability dashboard
| Category | Technology | |----------|-----------|| | Framework | .NET 10.0 | | Orchestration | .NET Aspire 13.1.0 | | API | ASP.NET Core Minimal APIs | | Database | PostgreSQL (Aspire-hosted) | | ORM | Entity Framework Core 10.0 | | CQRS | MediatR 13.1 | | Validation | FluentValidation 12.1 | | Documentation | OpenAPI/Swagger |
- .NET 10 SDK (or later)
- Docker Desktop (for PostgreSQL container)
- Visual Studio 2022 17.12+ or VS Code with C# extension
- .NET Aspire workload installed
dotnet workload update
dotnet workload install aspire- Open
DDDPlayground.sln - Set
DDDPlayground.AppHostas the startup project - Press
F5or click "Run"
# From solution root
cd src\DDDPlayground.AppHost
# Run the application
dotnet runOnce running, you'll see:
-
Aspire Dashboard: http://localhost:15000
- View traces, metrics, logs
- Monitor service health
- Access PostgreSQL admin (pgAdmin)
-
API Service: https://localhost:7001 (or port shown in console)
- Swagger UI: https://localhost:7001/openapi
- API endpoints: https://localhost:7001/api/orders
The database is automatically created on first run. If you need to manage migrations manually:
# Add a new migration (from solution root)
dotnet ef migrations add <MigrationName> --project src/DDDPlayground.Infrastructure --startup-project src/DDDPlayground.ApiService
# Update database
dotnet ef database update --project src/DDDPlayground.Infrastructure --startup-project src/DDDPlayground.ApiServiceDDDPlayground/
βββ .github/ # GitHub Actions, issue templates
βββ DDDPlayground.AppHost/ # Aspire orchestration host
β βββ AppHost.cs # Configures PostgreSQL, pgAdmin, API service
βββ DDDPlayground.ServiceDefaults/ # Aspire shared configuration
β βββ Extensions.cs # Service defaults (telemetry, health checks)βββ BenchmarkSuite/ # Performance benchmarks
β βββ BenchmarkSuite.csproj
β βββ DDDPlaygroundBenchmarks.cs
β βββ Program.csβββ DDDPlayground.Domain/ # Pure domain logic (ZERO dependencies)
β βββ Orders/
β β βββ Order.cs # Aggregate root with business logic
β β βββ OrderItem.cs # Entity within aggregate
β β βββ OrderStatus.cs # Enum
β β βββ IOrderRepository.cs # Repository interface
β βββ ValueObjects/
β β βββ Money.cs # Value object with operators
β β βββ OrderId.cs # Strongly-typed ID
β β βββ CustomerId.cs # Strongly-typed ID
β β βββ ProductId.cs # Strongly-typed ID
β β βββ Email.cs # Validated email
β βββ Services/
β β βββ PricingService.cs # Domain service
β βββ Events/
β β βββ OrderConfirmedEvent.cs # Domain event
β β βββ OrderCreatedEvent.cs # Domain event
β βββ Exceptions/
β βββ DomainException.cs # Base domain exception
βββ DDDPlayground.Application/ # Use case orchestration
β βββ Orders/
β β βββ CreateOrder/
β β β βββ CreateOrderCommand.cs # Command DTO
β β β βββ CreateOrderHandler.cs # Command handler
β β βββ ConfirmOrder/
β β β βββ ConfirmOrderCommand.cs
β β β βββ ConfirmOrderHandler.cs
β β βββ GetOrder/
β β β βββ GetOrderQuery.cs # Query DTO
β β β βββ GetOrderHandler.cs # Query handler
β β βββ EventHandlers/ # Domain event handlers
β β β βββ OrderConfirmedEventHandler.cs
β β β βββ OrderCreatedEventHandler.cs
β β βββ Notifications/ # MediatR notifications
β β βββ OrderResponse.cs # Response DTO with manual mapping
β βββ Common/
β β βββ IUnitOfWork.cs # Transaction boundary
β βββ DependencyInjection.cs # MediatR registration
βββ DDDPlayground.Infrastructure/ # Persistence implementation
β βββ Persistence/
β β βββ Models/ # Separate persistence entities
β β β βββ OrderEntity.cs # EF Core entity (NOT domain Order)
β β β βββ OrderItemEntity.cs # EF Core entity
β β βββ Mappers/ # Manual domain β persistence mapping
β β β βββ OrderMapper.cs # ToEntity() / ToDomain()
β β β βββ OrderItemMapper.cs
β β βββ Configurations/ # EF Core Fluent API
β β β βββ OrderEntityConfiguration.cs
β β β βββ OrderItemEntityConfiguration.cs
β β βββ Repositories/
β β β βββ OrderRepository.cs # IOrderRepository implementation
β β βββ Migrations/ # EF Core migrations (empty - auto-migrate on startup)
β β βββ Extensions/ # DbContext extensions
β β βββ AppDbContext.cs # DbContext with DbSet<OrderEntity>
β β βββ AppDbContextFactory.cs # Design-time DbContext factory
β βββ DependencyInjection.cs # Repository registration
βββ DDDPlayground.ApiService/ # HTTP API layer
β βββ Endpoints/
β β βββ OrderEndpoints.cs # Minimal API endpoints with MapGroup
β βββ Models/
β β βββ CreateOrderRequest.cs # API request DTOs
β βββ Program.cs # Aspire + Minimal API setup
β βββ appsettings.json # Configuration
β βββ DDDPlayground.ApiService.http # HTTP client test file
βββ .gitignore # Git ignore rules
βββ DDDPlayground.sln # Solution file
βββ MVP.md # Detailed MVP specification
βββ README.md # This file
All endpoints are prefixed with /api/orders.
POST /api/orders
Content-Type: application/json
{
"customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"items": [
{
"productId": "8fa85f64-5717-4562-b3fc-2c963f66afa6",
"quantity": 2,
"unitPrice": 99.99,
"currency": "USD"
}
]
}{
"id": "1fa85f64-5717-4562-b3fc-2c963f66afa6",
"customerId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"status": "Draft",
"subtotalAmount": 199.98,
"subtotalCurrency": "USD",
"discountAmount": 0.0,
"discountCurrency": "USD",
"totalAmount": 199.98,
"totalCurrency": "USD",
"createdAt": "2025-11-01T12:00:00Z",
"confirmedAt": null,
"items": [...]
}GET /api/orders/{id}Response: 200 OK (same structure as Create response)
POST /api/orders/{id}/confirmResponse: 200 OK (returns updated order with status: "Confirmed" and confirmedAt timestamp)
Use the included DDDPlayground.ApiService.http file with Visual Studio or VS Code REST Client extension.
-
Domain Model: src/DDDPlayground.Domain/Orders/Order.cs
- Rich domain model with encapsulation
- Business rules enforcement
- Factory methods and reconstitution
-
Persistence Separation: src/DDDPlayground.Infrastructure/Persistence/
Models/OrderEntity.cs- Separate EF entityMappers/OrderMapper.cs- Manual mappingConfigurations/OrderEntityConfiguration.cs- Fluent API
-
CQRS Pattern: src/DDDPlayground.Application/Orders/
- Commands vs. Queries
- Handler responsibilities
- DTO mapping
-
Minimal APIs: src/DDDPlayground.ApiService/Endpoints/OrderEndpoints.cs
- MapGroup organization
- TypedResults for type safety
- Static handlers for performance
// β Anemic (all properties public, no logic)
public class Order
{
public Guid Id { get; set; }
public string Status { get; set; }
}
// β
Rich (encapsulated, with business logic)
public class Order
{
public OrderId Id { get; private set; }
public OrderStatus Status { get; private set; }
public void Confirm()
{
if (Status != OrderStatus.Draft)
throw new DomainException("Only draft orders can be confirmed");
Status = OrderStatus.Confirmed;
}
}// β Domain model with EF attributes
public class Order
{
[Key]
public Guid Id { get; set; }
[Column("order_status")]
public string Status { get; set; }
}
// β
Separate domain and persistence models
// Domain: Order.cs (pure business logic)
public sealed class Order { /* ... */ }
// Persistence: OrderEntity.cs (EF Core entity)
public class OrderEntity { /* ... */ }
// Mapper: OrderMapper.cs (explicit transformation)
public static class OrderMapper
{
public static OrderEntity ToEntity(Order order) { /* ... */ }
public static Order ToDomain(OrderEntity entity) { /* ... */ }
}- .NET Aspire Documentation
- Domain-Driven Design by Eric Evans
- Clean Architecture by Robert C. Martin
- ASP.NET Core Minimal APIs
This project is licensed under the MIT License - see the LICENSE file for details.