A starting point for Clean Architecture with ASP.NET Core. Clean Architecture is just the latest in a series of names for the same loosely-coupled, dependency-inverted architecture. You will also find it named hexagonal, ports-and-adapters, or onion architecture.
Learn more about Clean Architecture and this template in NimblePros' Introducing Clean Architecture course. Use code ARDALIS to save 20%.
This architecture is used in the DDD Fundamentals course by Steve Smith and Julie Lerman.
| Technology | Purpose |
|---|---|
| .NET 10 | Runtime (preview) |
| FastEndpoints | REPR pattern API (not Controllers) |
| Mediator | CQRS with source generation (not MediatR) |
| Ardalis.Result | Operation result pattern |
| Ardalis.Specification | Repository + specification pattern |
| Vogen | Strongly-typed value objects (IDs, names) |
| SmartEnum | Type-safe enumerations |
| EF Core 10 | ORM with SQL Server / SQLite |
| Serilog | Structured logging |
| .NET Aspire | Service orchestration & OpenTelemetry |
| Testcontainers | Docker-based integration testing |
| Project | Layer | Responsibility |
|---|---|---|
| Clean.Architecture.Core | Domain | Entities, aggregates, value objects (Vogen), specifications, domain events, interfaces (IRepository, IEmailSender), domain services |
| Clean.Architecture.UseCases | Application | CQRS commands/queries, handlers (ICommandHandler/IQueryHandler), DTOs, query service interfaces |
| Clean.Architecture.Infrastructure | Infrastructure | EF Core AppDbContext, EfRepository<T>, migrations, query services, email sender, EventDispatchInterceptor |
| Clean.Architecture.Web | Interface | FastEndpoints (REPR), request/response/validator DTOs, ResultExtensions, Serilog + Aspire config |
| Clean.Architecture.ServiceDefaults | Cross-cutting | Aspire service defaults (OpenTelemetry, health checks, service discovery, HTTP resilience) |
| Clean.Architecture.AspireHost | Orchestration | Aspire host for local development orchestration |
Dependency rule: Core <- UseCases <- Infrastructure, Core <- UseCases <- Web. Core has zero external dependencies.
Client → FastEndpoint → FluentValidation → IMediator.Send(Command/Query)
→ Handler (UseCases) → IRepository<T> / IReadRepository<T> (Core interfaces)
→ EfRepository<T> (Infrastructure implementation) → EF Core → Database
← Result<T> ← ResultExtensions → Typed HTTP Result (200/201/204/400/404)
CQRS separation: Commands use IRepository<T> for writes. Queries use IReadRepository<T> for reads.
- .NET 10 SDK (preview)
- Docker Desktop (optional, for Testcontainers functional tests)
# Restore and build
dotnet build Clean.Architecture.slnx
# Run the web app
dotnet run --project src/Clean.Architecture.Web
# Run all tests
dotnet test Clean.Architecture.slnx
# Run a specific test project
dotnet test tests/Clean.Architecture.UnitTests
dotnet test tests/Clean.Architecture.IntegrationTests
dotnet test tests/Clean.Architecture.FunctionalTestscd src/Clean.Architecture.Web
dotnet ef migrations add MigrationName \
-c AppDbContext \
-p ../Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj \
-s Clean.Architecture.Web.csproj \
-o Data/Migrations
dotnet ef database update \
-c AppDbContext \
-p ../Clean.Architecture.Infrastructure/Clean.Architecture.Infrastructure.csproj \
-s Clean.Architecture.Web.csprojAll endpoints are under the Contributors tag. OpenAPI docs available at /scalar/v1 when running.
| Method | Route | Description | Success | Error |
|---|---|---|---|---|
POST |
/Contributors |
Create a new contributor | 201 Created |
400 Validation |
GET |
/Contributors/{id} |
Get contributor by ID | 200 OK |
404 Not Found |
GET |
/Contributors?page=1&per_page=10 |
List contributors (paginated) | 200 OK |
400 Invalid params |
PUT |
/Contributors/{id} |
Update contributor name | 200 OK |
404 / 400 |
DELETE |
/Contributors/{id} |
Delete contributor | 204 No Content |
404 Not Found |
Create a contributor:
curl -X POST https://localhost:5001/Contributors \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "phoneNumber": "+1 555-1234567"}'Get contributor by ID:
curl https://localhost:5001/Contributors/1List with pagination (Link header):
curl https://localhost:5001/Contributors?page=2&per_page=10Update a contributor:
curl -X PUT https://localhost:5001/Contributors/1 \
-H "Content-Type: application/json" \
-d '{"id": 1, "name": "Jane Doe"}'Delete a contributor:
curl -X DELETE https://localhost:5001/Contributors/1Connection string priority: Aspire cleanarchitecture > SQL Server DefaultConnection (Windows or USE_SQL_SERVER=true) > SQLite SqliteConnection.
Dev startup uses EnsureCreatedAsync (SQLite) or MigrateAsync (SQL Server) + seed data (27 contributors).
- FastEndpoints (REPR) instead of Controllers — one endpoint class per route
- Mediator with source generation instead of MediatR — handlers return
ValueTask, notTask - Ardalis.Result — all handlers return
Result<T>, mapped to typed HTTP results viaResultExtensions - Vogen for strongly-typed IDs (
ContributorId) and value objects (ContributorName) via source generation - Domain Events — dual dispatch: entity-level via
RegisterDomainEvent()+EventDispatchInterceptor(EF interceptor), and explicitIMediator.Publish()from domain services - Central Package Management — all versions in
Directory.Packages.props
dotnet new install Ardalis.CleanArchitecture.Template
dotnet new clean-arch -n MyProjectSee full documentation for template options and migration guides.
- Introducing Clean Architecture Course (NimblePros)
- DDD Fundamentals (Pluralsight)
- Live Stream Recordings
- DotNetRocks Podcast
🏫 Contact NimblePros for Clean Architecture or DDD training for your team.
If you like or are using this project to learn or start your solution, please give it a star. Thanks!
Or if you're feeling really generous, we now support GitHub sponsorships - see the button above.