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. Each layer gets a before/after in real .NET code, along with how CQRS with MediatR fits naturally into the structure.
The dependency direction becomes easier to enforce with the SOLID principles in .NET. For request-level access decisions at the API edge, review ASP.NET Core authorization types.
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 read and write models at the application boundary. Commands change state and may return nothing, an ID, or a result needed by the caller; queries return data and should not introduce business side effects. The useful rule is explicit intent, not a universal return-type restriction.
MediatR is one popular in-process dispatcher for this style, not a requirement or .NET standard. Direct application-service calls can preserve the same boundaries with less indirection. Whichever mechanism you choose, keep the database mutation and outbox insert in one transaction; publish external side effects from the outbox after commit.
Quick reference
- Command: requests a state change and returns only what its contract needs — void, ID, or an explicit result are all valid.
- Query: returns data, no mutation. IRequest<OrderDto> — separate read model from write model.
- MediatR pipeline behaviors can add cross-cutting concerns; direct decorators or middleware are valid alternatives.
- Commit aggregate changes and an outbox record atomically; publish email or broker messages after commit with idempotent retries.
- 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.
- Expose AddInfrastructure from Infrastructure; call it in WebApi's Program.cs composition root.
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
Separate class-library projects make dependency direction visible through explicit project references. The compiler prevents Domain from using EF Core only while Domain has no EF package/project reference; adding that reference would compile, so enforce the intended graph in review or with architecture tests.
A typical solution has MyApp.Domain (no outer references), MyApp.Application (references Domain), MyApp.Infrastructure (references Application and Domain), MyApp.WebApi (the composition root, references Application and Infrastructure to register concrete adapters), and tests with deliberate references.
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 and Infrastructure at the composition root; controllers depend on application contracts.
- 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 expose dependency direction; architecture tests or CI rules catch forbidden references someone could otherwise add.
Key takeaway
Clean Architecture is a discipline of dependency management, not a promise that every feature needs MediatR or that infrastructure is cost-free to replace. Keep business rules independent, choose explicit use-case boundaries, and let the WebApi composition root select concrete adapters. Use transactions and an outbox when a command couples database state to external side effects.
Practice (25 min): run dotnet list reference for Domain, Application, Infrastructure, and WebApi. Assert Domain has no outer reference, Application references only Domain, and WebApi references Infrastructure only for composition. Add an architecture test that fails when a Domain type depends on an EF Core namespace, then run it once with a deliberate forbidden dependency to prove the guard works.
Related Articles
Explore this topic