Clean Architecture in .NET: Layers, CQRS, and the Dependency Rule
Most .NET projects start clean and become entangled within six months. Controllers call repositories that call other services that reach back into controllers. Business logic lives in HTTP action methods. Tests require spinning up a database. A new developer can't tell where to put new code without reading existing code first.
Clean Architecture — popularized by Robert Martin and refined by the .NET community — is a set of structural rules that prevent this entanglement. The central rule is simple: dependencies only point inward. Domain code knows nothing about databases, HTTP, or infrastructure. Infrastructure knows everything about domain, but not the other way around. This article walks through each layer, shows the before/after in real .NET code, and explains how CQRS with MediatR fits naturally into the structure.
The Four Layers
Clean Architecture organizes code into four concentric layers. The innermost layer is Domain: entities, value objects, domain events, and business rules. Domain code has zero dependencies on frameworks, databases, or external services. It's pure C# classes with behavior.
The Application layer contains use cases — what the system can do. It orchestrates domain objects to fulfill user intentions: PlaceOrder, CancelSubscription, GenerateReport. It defines interfaces (IOrderRepository, IEmailSender) that it needs, but doesn't implement them. The Infrastructure layer implements those interfaces: EfOrderRepository, SendGridEmailSender, StripePaymentGateway. The outermost Presentation layer is HTTP controllers, gRPC services, background jobs — the entry points that turn external requests into application commands.
Quick reference
- Domain: entities, value objects, domain events, domain services. Zero external dependencies.
- Application: use cases (commands/queries), interfaces, DTOs, validation. Depends on Domain only.
- Infrastructure: EF Core DbContext, external HTTP clients, file system, email providers. Depends on Application + Domain.
- Presentation: ASP.NET Core controllers, minimal API endpoints, SignalR hubs. Depends on Application only.
- The Dependency Rule: source code dependencies point inward. Outer layers know about inner layers. Inner layers know nothing about outer layers.
- Project structure: one .csproj per layer, with explicit project references enforcing the dependency rule at compile time.
Remember this
The dependency rule is the architecture. If an inner layer needs to reference an outer layer, you're violating the structure.
Domain Layer: Entities and Value Objects
Domain entities are not just data bags. They encapsulate behavior and enforce their own invariants. An Order doesn't let you add a negative quantity — the domain object rejects it. A Product knows how to reserve stock and throws a domain exception when stock is insufficient. The domain layer is where the core business rules live, isolated from all infrastructure concerns.
Value Objects represent concepts with no identity — they're equal when their data is equal. Money, Address, Email, OrderStatus are natural value objects. Using proper value objects instead of primitives (strings and ints) eliminates entire classes of bugs: you can't accidentally compare a price in USD with a price in EUR, or pass a shipping address where a billing address is expected.
Quick reference
- Entities have identity (Id). Two entities with the same Id are the same entity, even with different field values.
- Value objects have no identity. Two Money(10, 'USD') objects are equal regardless of reference.
- Domain exceptions (DomainException) signal business rule violations — not infrastructure failures.
- Aggregate root: the entry point to a cluster of entities. External code only interacts with the root (Order, not OrderItem).
- Domain events: publish what happened within the domain. Collected and dispatched by the application layer.
- Keep the domain layer dependency-free. If you're importing EF Core or ASP.NET in your domain project, stop.
Remember this
Rich domain models encode business rules as behavior on entities. They're self-validating, framework-free, and trivially testable in isolation.
CQRS with MediatR
CQRS (Command Query Responsibility Segregation) separates reads (queries) from writes (commands) at the application layer. Commands change state and return nothing (or just an ID). Queries return data and change nothing. This separation keeps handlers focused — a command handler doesn't accidentally return data; a query handler doesn't accidentally mutate state.
MediatR is the standard .NET library for this pattern. Each use case is a command or query class, and a handler class. The controller dispatches a command to MediatR, which finds the registered handler and executes it. Controllers become thin — they translate HTTP to commands and commands to HTTP responses, with no business logic.
Quick reference
- Command: mutates state, returns void or an ID. IRequest<Guid> or IRequest (no return).
- Query: returns data, no mutation. IRequest<OrderDto> — separate read model from write model.
- MediatR pipeline behaviors: add cross-cutting concerns (logging, validation, transactions) without touching handlers.
- FluentValidation + MediatR: add a ValidationBehavior that runs validators before every command handler.
- One handler per command/query — no god-service with 30 methods.
- Unit test handlers directly — inject mock repositories, assert on domain behavior. No HTTP setup needed.
Remember this
CQRS with MediatR makes every use case a named, testable class. Controllers become HTTP adapters with zero business logic.
Repository Pattern and Infrastructure
The application layer defines interfaces for what it needs from infrastructure — IOrderRepository, IEmailSender, IStorageService. The Infrastructure layer implements them using concrete technology: EF Core, SendGrid, Azure Blob Storage. Domain and Application layers never reference EF Core, never call HttpClient directly, never know if data lives in Postgres or an in-memory store.
This inversion enables testing: in unit tests, inject a fake in-memory repository. In integration tests, inject the real EF Core repository. The application handler is the same code in both cases.
Quick reference
- Define repository interfaces in the Application layer. Implement them in Infrastructure.
- Keep repositories focused: one aggregate root per repository. Don't create a generic IRepository<T>.
- EF Core DbContext lives entirely in the Infrastructure project — domain and application never reference it.
- For read queries (CQRS), bypass the repository and query directly from EF using projections. Repositories are for aggregate writes.
- Avoid the N+1 problem in read queries: use .Include() or projections (.Select()), not lazy loading.
- Register implementations with AddScoped (per HTTP request) in Program.cs using DI.
Remember this
Interfaces in Application, implementations in Infrastructure. The application never knows if it's talking to EF Core, Dapper, or an in-memory fake.
Project Structure and Solution Layout
The cleanest way to enforce Clean Architecture in .NET is with separate class library projects, each with explicit project references. The compiler enforces the dependency rule: if you accidentally add an EF Core reference to the Domain project, the build fails.
A typical solution has four or five projects: MyApp.Domain (no references), MyApp.Application (references Domain), MyApp.Infrastructure (references Application and Domain, contains EF Core, external SDKs), MyApp.WebApi (references Application, wires up DI and HTTP), and MyApp.Tests (references all layers, uses test fakes and integration test helpers).
Quick reference
- MyApp.Domain: no project references. Classes only — entities, value objects, domain events, exceptions.
- MyApp.Application: references Domain. MediatR handlers, interfaces, validators, DTOs.
- MyApp.Infrastructure: references Application + Domain. EF Core, HttpClient wrappers, third-party SDKs.
- MyApp.WebApi: references Application (not Infrastructure directly — uses DI). Controllers, middleware, Swagger.
- Infrastructure registration: use extension methods (AddInfrastructure(services, config)) called from WebApi startup.
- Vertical slices variant: organize by feature (Orders/, Products/) within layers instead of horizontal layers. Works well with CQRS.
Remember this
Separate projects per layer and let project references enforce the dependency rule at compile time. No runtime architecture checks needed.
Clean Architecture is not a framework — it's a discipline of dependency management. The rules are few: dependencies point inward, business logic lives in the domain, infrastructure is swappable, use cases are named objects. Following these rules has compounding returns: every new feature is a new handler class, every refactor is scoped to one layer, every test runs without a database.
The biggest mistake teams make is treating it as a folder structure rather than a dependency structure. A folder called 'Domain' that imports EF Core is not Clean Architecture. The architecture lives in the project references, the interfaces, and the discipline of keeping business rules free of infrastructure concerns. Get that right, and the folder names become secondary.
Related Articles
Explore this topic