.NET Project Architectures & Folder Structures — Layered, Clean, Onion, Vertical Slice
In short: Presentation (UI), business logic (services/BLL) and data access (DAL/repositories), each calling only the layer below.
- It separates concerns and lets teams work on layers independently.
- Layers are logical; tiers are physical machines.
- Weakness: business logic often depends directly on data access, making it hard to test and change.
In short: An architecture that places the domain (entities, business rules) and application use cases at the centre and frameworks, UI and databases at the outside.
- The dependency rule: source-code dependencies only point inward — the domain depends on nothing;
- the application defines interfaces that infrastructure implements;
- the web project is the composition root that wires everything with DI.
In short: src/ with Domain (entities, value objects), Application (use cases, DTOs, interfaces, validation), Infrastructure (EF Core DbContext, migrations, external services) and Api (controllers/endpoints, middleware, Program.cs) projects;
tests/with unit, integration and functional test projects;- shared settings in Directory.Build.props, central package versions, global.json, .editorconfig;
- Dockerfile and pipelines alongside.
In short: Same core idea with different vocabulary: business logic at the centre, independent of infrastructure, connected through interfaces.
- Hexagonal talks about ports (interfaces) and adapters (implementations/drivers);
- Onion about concentric rings (domain model, domain services, application services, infrastructure/UI);
- Clean about entities, use cases, interface adapters and frameworks with the explicit dependency rule.
- In .NET they produce similar project structures.
In short: Layered organises by technical concern (controllers, services, repositories) so one feature spans many folders and projects.
- Vertical Slice organises by feature/use case — request, validation, handler, endpoint together — so changes are local and each slice can choose its data access.
- Slices can still share a domain model.
- Vertical slices suit CQRS-style APIs with many independent use cases.
In short: In the Application (or Domain) layer — the inner layer defines what it needs (IOrderRepository), and Infrastructure implements it.
- This is Dependency Inversion: the business logic doesn't depend on EF Core or SQL, can be unit-tested with fakes, and the implementation can change without touching use cases.
In short: A single deployable application split into well-defined modules by business capability, each with its own code, data (separate schema) and public API;
- modules interact only through contracts or events.
- You get microservice-like boundaries without distributed-system complexity, and modules can be extracted into services later if needed.
In short: A layer is a logical grouping of code by responsibility (UI, business, data).
- A tier is a physical deployment boundary (client device, web server, application server, database server).
- You can deploy several layers on one tier; crossing tiers adds network calls, latency and failure modes.
In short: Command Query Responsibility Segregation: separate models/handlers for writes (commands that change state) and reads (queries that return data).
- It allows optimised read models (Dapper, views, caches) and rich write models.
- MediatR is just one way to dispatch requests to handlers with pipeline behaviours; you can implement CQRS with plain interfaces and DI.
Make rules explicit (ADRs, diagrams), enforce dependencies with project references (inner projects simply can't reference outer ones) and architecture tests (NetArchTest/ArchUnitNET in CI), code reviews with a checklist, templates for new features, internal visibility (internal + InternalsVisibleTo) to hide implementation details, and regular refactoring.
In short: An aggregate is a cluster of domain objects treated as one unit for changes, with a root entity that enforces invariants (Order with OrderLines).
- Outside code references only the root and changes go through its methods; one transaction modifies one aggregate.
- This defines repository boundaries (one repository per aggregate root) and keeps business rules in one place.
In short: The single place where the application's object graph is wired together — in ASP.NET Core, Program.cs (often via AddApplication()/AddInfrastructure() extension methods) registers interfaces with their implementations in the DI container.
- Only the composition root knows about all projects; inner layers depend only on abstractions.
In short: Add characterization tests around key endpoints first.
- Extract business logic into application services/handlers with interfaces for data access and email;
- move SQL into repositories or query classes in an infrastructure project;
- introduce DI registration;
- keep controllers thin (bind, call, return).
- Do it feature by feature (strangler within the codebase), not as a big-bang rewrite, and agree on the target structure in an ADR.
In short: Probably over-engineered: more projects, mapping and indirection than the problem needs, slowing the team.
- A single project with clear folders (Features or Controllers/Services/Data) and good tests is enough; keep the door open by keeping logic out of controllers.
- Choose architecture by complexity and expected lifetime, not by habit.
In short: Monorepo: src/Shop.Api, src/Shop.Worker (BackgroundService/queue consumers), shared Shop.Application/Shop.Domain/Shop.Infrastructure projects used by both, web/shop-ui (Angular workspace), tests/ for each, deploy/ with Dockerfiles per deployable, CI pipelines that build only what changed.
- Or with .NET Aspire, an AppHost project orchestrating API, worker, database and the front end locally.