-
Notifications
You must be signed in to change notification settings - Fork 15
Description
Description
The current folder structure follows a traditional layered architecture (Controllers → Services → Repositories → Data) which, while functional, has some architectural limitations:
src/Dotnet.Samples.AspNetCore.WebApi/
├── Controllers/ # HTTP/API concerns
├── Services/ # Business logic + orchestration
├── Repositories/ # Data access abstractions & implementations
├── Data/ # EF Core DbContext
├── Models/ # Mix of entities + DTOs ⚠️
├── Enums/ # Domain enums
├── Validators/ # Input validation
├── Mappings/ # Object mapping
├── Utilities/ # Various utilities
├── Configurations/ # Infrastructure configuration
└── Extensions/ # Service registration
Current Architecture Issues
- Domain entities mixed with DTOs in
Models/(e.g.,Player.csalongsidePlayerRequestModel.cs) - Business logic scattered across multiple layers
- Dependencies flow in all directions, making testing difficult
- Tight coupling between infrastructure and business logic
- Limited ability to swap implementations (database, external services)
This issue proposes migrating to a Clean Architecture-inspired folder structure to improve maintainability, testability, and adherence to SOLID principles.
Minimal vs Full Clean Architecture
Based on the Ardalis CleanArchitecture patterns, there are two valid approaches:
| Aspect | Minimal (Recommended) | Full |
|---|---|---|
| Structure | Single project with layer folders | 4+ separate projects |
| Separation | Convention-based (folders) | Compiler-enforced (project references) |
| Complexity | Lower | Higher |
| Best For | MVPs, learning projects, small teams | Large enterprise apps, multiple teams |
| Migration | Can grow to Full later | N/A |
| Naming | No redundancy issues | Requires careful naming (avoid .WebApi.WebApi) |
When to Use Each
Choose Minimal when:
- Single bounded context (like our
Playerentity) - Small team or solo development
- Learning-focused or PoC project
- Want Clean Architecture benefits without solution complexity
Choose Full when:
- Multiple bounded contexts or domains
- Large teams needing strict boundaries
- Need compile-time dependency enforcement
- Planning microservices extraction
Proposed Solution (Minimal Approach)
Restructure the existing project using folder-based layers:
src/Dotnet.Samples.AspNetCore.WebApi/
├── Core/ # Domain Layer
│ ├── Entities/
│ │ └── Player.cs # Pure domain entity
│ └── Enums/
│ ├── Enumeration.cs
│ └── Position.cs
│
├── Application/ # Application Layer
│ ├── Contracts/
│ │ ├── DTOs/
│ │ │ ├── PlayerRequestDto.cs # Input DTOs
│ │ │ └── PlayerResponseDto.cs # Output DTOs
│ │ ├── Persistence/
│ │ │ ├── IPlayerRepository.cs # Repository contracts
│ │ │ └── IRepository.cs # Generic contracts
│ │ └── Services/
│ │ └── IPlayerService.cs # Service contracts
│ ├── Services/
│ │ └── PlayerService.cs # Business logic
│ ├── Validators/
│ │ └── PlayerRequestDtoValidator.cs # Input validation
│ ├── Mappings/
│ │ └── PlayerMappingProfile.cs # AutoMapper profiles
│ └── Exceptions/ # Custom business exceptions
│
├── Infrastructure/ # Infrastructure Layer
│ ├── Persistence/
│ │ ├── PlayerDbContext.cs # EF Core DbContext
│ │ ├── Repositories/
│ │ │ ├── Repository.cs # Generic implementation
│ │ │ └── PlayerRepository.cs # Specific implementation
│ │ ├── Configurations/
│ │ │ └── PlayerConfiguration.cs # EF entity configuration
│ │ └── Migrations/ # EF Core migrations
│ └── Extensions/
│ └── InfrastructureServiceExtensions.cs # Infrastructure DI setup
│
├── Api/ # Presentation Layer
│ ├── Controllers/
│ │ └── PlayerController.cs
│ ├── Configurations/
│ │ ├── RateLimiterConfiguration.cs
│ │ └── AuthorizeCheckOperationFilter.cs
│ ├── Extensions/
│ │ └── ApiServiceExtensions.cs # API layer DI setup
│ ├── Middleware/ # Custom middleware
│ └── Filters/ # Action filters
│
├── Utilities/ # Shared utilities
│ ├── HttpContextUtilities.cs
│ ├── PlayerData.cs
│ └── SwaggerUtilities.cs
│
└── Program.cs # Application entry point
Namespace Convention
Namespaces should mirror the folder structure for clarity:
// Domain layer
namespace Dotnet.Samples.AspNetCore.WebApi.Core.Entities;
namespace Dotnet.Samples.AspNetCore.WebApi.Core.Enums;
// Application layer
namespace Dotnet.Samples.AspNetCore.WebApi.Application.Contracts.DTOs;
namespace Dotnet.Samples.AspNetCore.WebApi.Application.Contracts.Persistence;
namespace Dotnet.Samples.AspNetCore.WebApi.Application.Services;
// Infrastructure layer
namespace Dotnet.Samples.AspNetCore.WebApi.Infrastructure.Persistence;
namespace Dotnet.Samples.AspNetCore.WebApi.Infrastructure.Persistence.Repositories;
// API layer
namespace Dotnet.Samples.AspNetCore.WebApi.Api.Controllers;Dependency Flow (Clean Architecture Principles)
Api → Application → Core
Infrastructure → Application → Core
Even without separate projects, maintain this flow by convention:
Core/has zero dependencies on other layersApplication/references onlyCore/Infrastructure/implements interfaces fromApplication/Api/orchestrates via dependency injection
Benefits of This Approach
1. Clear Separation of Concerns
- Core: Pure business entities and rules (no infrastructure concerns)
- Application: Use cases, business logic, and contracts
- Infrastructure: Technical implementations (database, caching, external APIs)
- Api: HTTP/presentation layer
2. Improved Testability
- Domain logic can be tested in complete isolation
- Application services can be tested with mocked repositories
- No need to mock database for business logic tests
3. Dependency Inversion
- Application defines
IPlayerRepositoryinterface - Infrastructure provides
PlayerRepositoryimplementation - Easy to swap SQLite for PostgreSQL, or add caching, without touching business logic
4. Explicit Boundaries
- DTOs separated from domain entities
- Clear API contracts via interfaces
- Infrastructure details hidden from business logic
5. Simpler Structure
- No solution restructuring needed
- No naming redundancy issues (avoids
.WebApi.WebApi) - Easy to navigate — single project, clear folder hierarchy
- Flexible — can upgrade to Full approach later if needed
Migration Strategy
All commands assume you're in the repository root directory.
Phase 1: Create Folder Structure
# Create Core (Domain) layer folders
mkdir -p src/Dotnet.Samples.AspNetCore.WebApi/Core/{Entities,Enums}
# Create Application layer folders
mkdir -p src/Dotnet.Samples.AspNetCore.WebApi/Application/{Contracts/{DTOs,Persistence,Services},Services,Validators,Mappings,Exceptions}
# Create Infrastructure layer folders
mkdir -p src/Dotnet.Samples.AspNetCore.WebApi/Infrastructure/{Persistence/{Repositories,Configurations,Migrations},Extensions}
# Create Api (Presentation) layer folders
mkdir -p src/Dotnet.Samples.AspNetCore.WebApi/Api/{Controllers,Configurations,Extensions,Middleware,Filters}Phase 2: Move Domain Layer (Core/)
# Move domain entity (keep it pure, remove any infrastructure concerns)
mv src/Dotnet.Samples.AspNetCore.WebApi/Models/Player.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Core/Entities/Player.cs
# Move enums to Core
mv src/Dotnet.Samples.AspNetCore.WebApi/Enums/Position.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Core/Enums/Position.cs
mv src/Dotnet.Samples.AspNetCore.WebApi/Enums/Enumeration.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Core/Enums/Enumeration.csAfter moving, update namespaces in each file to match the new folder structure.
Phase 3: Move Application Layer
# Move DTOs (rename Model → Dto)
mv src/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerRequestModel.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Application/Contracts/DTOs/PlayerRequestDto.cs
mv src/Dotnet.Samples.AspNetCore.WebApi/Models/PlayerResponseModel.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Application/Contracts/DTOs/PlayerResponseDto.cs
# Move repository interfaces (contracts)
mv src/Dotnet.Samples.AspNetCore.WebApi/Repositories/IRepository.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Application/Contracts/Persistence/IRepository.cs
mv src/Dotnet.Samples.AspNetCore.WebApi/Repositories/IPlayerRepository.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Application/Contracts/Persistence/IPlayerRepository.cs
# Move service interface (contract)
mv src/Dotnet.Samples.AspNetCore.WebApi/Services/IPlayerService.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Application/Contracts/Services/IPlayerService.cs
# Move service implementation
mv src/Dotnet.Samples.AspNetCore.WebApi/Services/PlayerService.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Application/Services/PlayerService.cs
# Move validators (rename to match DTO naming)
mv src/Dotnet.Samples.AspNetCore.WebApi/Validators/PlayerRequestModelValidator.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Application/Validators/PlayerRequestDtoValidator.cs
# Move mappings
mv src/Dotnet.Samples.AspNetCore.WebApi/Mappings/PlayerMappingProfile.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Application/Mappings/PlayerMappingProfile.csAfter moving, update namespaces and rename classes (PlayerRequestModel → PlayerRequestDto, etc.).
Phase 4: Move Infrastructure & API Layers
Infrastructure:
# Move DbContext
mv src/Dotnet.Samples.AspNetCore.WebApi/Data/PlayerDbContext.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Infrastructure/Persistence/PlayerDbContext.cs
# Move migrations folder
mv src/Dotnet.Samples.AspNetCore.WebApi/Migrations/* \
src/Dotnet.Samples.AspNetCore.WebApi/Infrastructure/Persistence/Migrations/
# Move repository implementations
mv src/Dotnet.Samples.AspNetCore.WebApi/Repositories/Repository.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Infrastructure/Persistence/Repositories/Repository.cs
mv src/Dotnet.Samples.AspNetCore.WebApi/Repositories/PlayerRepository.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Infrastructure/Persistence/Repositories/PlayerRepository.cs
# Move infrastructure-related extensions
mv src/Dotnet.Samples.AspNetCore.WebApi/Extensions/ServiceCollectionExtensions.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Infrastructure/Extensions/InfrastructureServiceExtensions.csAPI:
# Move controllers
mv src/Dotnet.Samples.AspNetCore.WebApi/Controllers/PlayerController.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Api/Controllers/PlayerController.cs
# Move API configurations
mv src/Dotnet.Samples.AspNetCore.WebApi/Configurations/RateLimiterConfiguration.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Api/Configurations/RateLimiterConfiguration.cs
mv src/Dotnet.Samples.AspNetCore.WebApi/Configurations/AuthorizeCheckOperationFilter.cs \
src/Dotnet.Samples.AspNetCore.WebApi/Api/Configurations/AuthorizeCheckOperationFilter.csPhase 5: Cleanup & Validation
# Remove empty old folders
rmdir src/Dotnet.Samples.AspNetCore.WebApi/Models
rmdir src/Dotnet.Samples.AspNetCore.WebApi/Enums
rmdir src/Dotnet.Samples.AspNetCore.WebApi/Services
rmdir src/Dotnet.Samples.AspNetCore.WebApi/Repositories
rmdir src/Dotnet.Samples.AspNetCore.WebApi/Data
rmdir src/Dotnet.Samples.AspNetCore.WebApi/Migrations
rmdir src/Dotnet.Samples.AspNetCore.WebApi/Controllers
rmdir src/Dotnet.Samples.AspNetCore.WebApi/Configurations
# Verify build succeeds
dotnet build
# Run all tests
dotnet testAfter cleanup, update all using statements across the codebase to reference the new namespaces.
Acceptance Criteria
- Core folder contains only pure domain logic (entities, enums, value objects)
- Application folder contains business logic and defines contracts (interfaces)
- Infrastructure folder implements Application interfaces
- Api folder contains only HTTP/presentation concerns
- Namespaces mirror folder structure (e.g.,
*.Core.Entities,*.Application.Services) - DTOs separated from domain entities (no more mixed
Models/folder) - Dependency flow maintained by convention (Core ← Application ← Infrastructure/Api)
- All existing tests pass after migration
- Build succeeds without errors or warnings
- Docker compose still functions correctly
- Database migrations continue to work
- API functionality remains unchanged (same endpoints, same behavior)
- Documentation updated (README reflects new structure)
Trade-offs
Pros ✅
- Simpler structure — no solution restructuring needed
- No naming redundancy — avoids
.WebApi.WebApiissue - Easy to navigate — single project, clear folder hierarchy
- Lower complexity — appropriate for learning-focused PoC
- Flexible — can upgrade to Full approach later if needed
- Same benefits — clear separation of concerns, testability improvements
- Better testability and maintainability
- Clear architectural boundaries
- Easy to swap implementations
- Follows industry best practices
Cons ⚠️
- Convention-based — layer boundaries enforced by code review, not compiler
- Easier to violate — developers could accidentally import wrong namespaces
- No compile-time protection — circular dependencies possible within project
- More folders to navigate
- Learning curve for developers new to Clean Architecture
Mitigation
- Use code review to enforce layer dependencies
- Consider adding architectural tests (e.g., ArchUnitNET) to validate layer rules
- Document namespace conventions clearly
Future: Full Clean Architecture
When the project grows beyond a single bounded context or requires stricter boundaries, migrate to separate projects:
src/
├── Dotnet.Samples.AspNetCore.WebApi.Core/ # Domain
├── Dotnet.Samples.AspNetCore.WebApi.Application/ # Use Cases (or .UseCases)
├── Dotnet.Samples.AspNetCore.WebApi.Infrastructure/
└── Dotnet.Samples.AspNetCore.WebApi.Api/ # Presentation (not .WebApi!)
Migration triggers:
- Multiple bounded contexts or aggregates
- Team growth requiring enforced boundaries
- Need to share domain logic across multiple applications
- Microservices extraction planned
Note: The presentation layer uses .Api suffix (not .WebApi) to avoid the redundant Dotnet.Samples.AspNetCore.WebApi.WebApi naming.
References
- Ardalis CleanArchitecture Template — includes both Full and Minimal approaches
- Clean Architecture by Robert C. Martin
- Microsoft .NET Application Architecture Guides
- eShopOnContainers Reference Architecture
- Jason Taylor's Clean Architecture Template
- Vladimir Khorikov - DDD and EF Core
- ArchUnitNET — for architectural testing