Roadmap › .NET Project Architectures & Folder Structures — Layered, Clean, Onion, Vertical Slice › Module: Architecture Styles
Lessons in this course (8)
3. Onion, Hexagonal and Clean Architecture
Intermediate★ Must knowLesson 3 of 8
All three share one idea: the domain and use cases are the centre; frameworks, databases and UIs are details at the edge, connected through interfaces (Dependency Inversion).
text
┌────────────────────────────────────────┐
│ Presentation (Api, Web, Blazor) │
│ ┌──────────────────────────────────┐ │
│ │ Infrastructure (EF Core, email, │ │
│ │ files, external APIs) │ │
│ │ ┌──────────────────────────┐ │ │
│ │ │ Application (use cases, │ │ │
│ │ │ interfaces, DTOs) │ │ │
│ │ │ ┌──────────────────┐ │ │ │
│ │ │ │ Domain (entities,│ │ │ │
│ │ │ │ value objects, │ │ │ │
│ │ │ │ domain rules) │ │ │ │
│ │ │ └──────────────────┘ │ │ │
│ │ └──────────────────────────┘ │ │
│ └──────────────────────────────────┘ │
└────────────────────────────────────────┘
dependencies point INWARD only- Hexagonal (ports & adapters): the application exposes ports (interfaces like
IOrderRepository,IPaymentGateway); adapters implement them (SQL adapter, Stripe adapter) or drive the app (REST controller, message consumer). - Onion: concentric rings — Domain Model → Domain Services → Application Services → outer ring (UI, infrastructure, tests).
- Clean Architecture: Entities → Use Cases → Interface Adapters → Frameworks & Drivers; same dependency rule.
Clean Architecture folder structure (ASP.NET Core):
text
src/
├── Shop.Domain/ no project references, no NuGet except tiny helpers
│ ├── Common/ BaseEntity.cs, ValueObject.cs, IDomainEvent.cs
│ ├── Entities/ Order.cs, OrderLine.cs, Customer.cs, Product.cs
│ ├── ValueObjects/ Money.cs, Address.cs, Email.cs
│ ├── Enums/ OrderStatus.cs
│ ├── Events/ OrderPlacedEvent.cs
│ └── Exceptions/ DomainException.cs
├── Shop.Application/ references Domain only
│ ├── Common/
│ │ ├── Interfaces/ IApplicationDbContext.cs, IEmailSender.cs, ICurrentUser.cs, IDateTime.cs
│ │ ├── Behaviours/ ValidationBehaviour.cs, LoggingBehaviour.cs (pipeline behaviours)
│ │ ├── Exceptions/ NotFoundException.cs, ValidationException.cs
│ │ └── Models/ PagedList.cs, Result.cs
│ ├── Orders/
│ │ ├── Commands/PlaceOrder/ PlaceOrderCommand.cs, PlaceOrderHandler.cs, PlaceOrderValidator.cs
│ │ ├── Queries/GetOrders/ GetOrdersQuery.cs, GetOrdersHandler.cs, OrderDto.cs
│ │ └── EventHandlers/ OrderPlacedEmailHandler.cs
│ └── DependencyInjection.cs AddApplication() extension
├── Shop.Infrastructure/ references Application (and Domain)
│ ├── Persistence/ ShopDbContext.cs, Configurations/OrderConfiguration.cs, Migrations/, Interceptors/
│ ├── Repositories/ (if used) OrderRepository.cs
│ ├── Services/ SmtpEmailSender.cs, DateTimeService.cs, BlobFileStorage.cs
│ ├── Identity/ identity/auth integration
│ └── DependencyInjection.cs AddInfrastructure(configuration)
└── Shop.Api/ references Application + Infrastructure (composition root)
├── Controllers/ or Endpoints/ OrdersController.cs / OrderEndpoints.cs
├── Middleware/ ExceptionHandler.cs
├── Filters/
├── Program.cs builder.Services.AddApplication().AddInfrastructure(cfg); app pipeline
└── appsettings.json
tests/
├── Shop.Domain.UnitTests/
├── Shop.Application.UnitTests/
├── Shop.Infrastructure.IntegrationTests/ (Testcontainers SQL Server)
└── Shop.Api.FunctionalTests/ (WebApplicationFactory)csharp
// Application defines what it needs...
public interface IEmailSender { Task SendAsync(string to, string subject, string body, CancellationToken ct); }
// ...Infrastructure provides it...
public sealed class SmtpEmailSender(IOptions<SmtpOptions> o) : IEmailSender { /* SmtpClient / MailKit */ }
// ...Api wires it (composition root)
builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();Benefits: domain logic testable without a database, swap infrastructure (SQL → Cosmos, SMTP → SendGrid) without touching use cases, framework upgrades isolated. Costs: more projects and mapping code — overkill for small CRUD apps.