Open the implementation details you need; print expands them.

Quick Reference

Layer Owns References Never Put Here
Domain Entities, value objects, aggregates, business invariants, domain events No project dependencies inside the solution EF Core attributes, HTTP models, logging frameworks, DTO mapping profiles
Application Use cases, command/query handlers, DTOs, validation, service interfaces Domain Controllers, database providers, SMTP clients, cloud SDK calls
Infrastructure Repository implementations, EF Core DbContext, migrations, external services Application, Domain types as needed by contracts Business decisions that should be testable without external systems
Presentation Controllers/endpoints, middleware, filters, DI composition, OpenAPI setup Application and Infrastructure for startup composition Domain logic, transaction orchestration, query implementation details

Understanding Clean Architecture

The Case for Clean Architecture: Taming Complexity

In many software projects, traditional layered architectures can inadvertently lead to tightly coupled systems. Business logic often becomes entangled with database details, UI frameworks, or third-party service integrations. This "spaghetti architecture" results in code that is hard to test, painful to change, and brittle when modified. Technology choices become almost irreversible, leading to expensive migrations or outdated systems.

Clean Architecture tackles these challenges head-on by championing the Dependency Rule. This fundamental principle dictates that source code dependencies can only point inwards. The core of your applicationβ€”the Domain (enterprise business rules) and Application (application-specific business rules) layersβ€”knows nothing about the outer layers like Infrastructure (databases, file systems, network calls) or Presentation (UI, Web API). Outer layers depend on abstractions (interfaces) defined by the inner layers, effectively inverting the control flow for technical details.

Benefits & Suitability
Key Benefits Unlocked:
  • Independent of Frameworks: The core business logic isn't tied to web frameworks, UI toolkits, or database technologies.
  • Enhanced Testability: Business rules can be unit-tested in isolation, without external dependencies, leading to faster and more reliable tests.
  • Independent of UI: The UI can change easily, without changing the rest of the system. A Web UI could be swapped for a console UI, for instance, without business rule changes.
  • Independent of Database: You can swap Oracle or SQL Server, for Mongo or BigTable. Your business rules are not bound to the database.
  • Independent of External Agencies: Your business rules don't know anything about the outside world.
  • Improved Maintainability & Flexibility: Changes to external concerns have minimal impact on core logic, making the system easier to evolve and adapt.
  • Promotes a development focus on core business logic, leading to more accurate implementation.
  • Helps maintain consistent coding practices, improving application stability and security.
  • Aids in quickly adding new features, APIs, and third-party components.
  • Implements abstraction effectively.
When is Clean Architecture Most Beneficial?
  • For applications with significant and complex business logic that forms the heart of their value proposition.
  • In long-lived projects expected to undergo evolution, maintenance, and potential technology shifts over many years.
  • When high degrees of testability and maintainability are paramount project goals.
  • If there's a strategic need for independence from specific external frameworks or technologies, ensuring future-proofing and adaptability.
  • When the software needs to closely follow Domain-Driven Design (DDD) principles.
  • When there is a need for the architecture to help enforce specific development policies and standards.

Important Note: Clean Architecture is not a silver bullet. It introduces a degree of initial setup complexity and requires discipline. For very simple CRUD applications or short-lived projects, the overhead might not be justified. However, for complex, evolving systems, the long-term benefits in resilience, adaptability, and reduced maintenance costs often far outweigh the upfront investment.

Core Principles

Core Philosophy
  • The Dependency Rule: The cornerstone. Promotes independence of core business logic from external concerns.
  • Abstraction via Interfaces: Inner layers define abstractions (interfaces); outer layers provide concrete implementations. This inverts traditional control flow for dependencies.
  • High Testability: Business logic (Domain & Application layers) can be unit-tested independently of UI, database, or any external service.
  • Enhanced Maintainability & Flexibility: The decoupling makes the system easier to understand, modify, and evolve. Changes in one area (e.g., database technology) are less likely to ripple through the entire codebase.
  • Focus on Core Logic: Business rules and entities are modeled within the Core (Domain) project, independent of other concerns.
  • Unidirectional Dependencies: All source code dependencies must flow inwards, towards the Core project. Outer layers depend on the Core; the Core does not depend on any outer layer.
  • Interface Definition: Inner layers (Domain, Application) define interfaces, and outer layers (Infrastructure, Presentation) implement them.
Key Motivators for Adoption
  • Tackling systems with complex and evolving business logic that needs protection from volatile external details.
  • Ensuring the longevity and adaptability of an application over an extended lifecycle.
  • Aligning with Domain-Driven Design (DDD) principles, placing the domain model at the application's heart.
  • Achieving superior testability by isolating business rules from infrastructure for reliable unit testing.
  • Gaining independence from specific technologies (databases, frameworks, UI), allowing for easier changes or deferral of decisions.

Layers Overview & Responsibilities

Domain Layer (Core)

Purpose: Contains enterprise-wide business logic, entities, and value objects. It's the innermost layer and has no dependencies on other layers in the solution.

Key Components
Key Components:
  • Entities: Core business objects with an identity, encapsulating business logic and data.
    // Domain/Entities/Product.cs
    public class Product
    {
        public Guid Id { get; private set; }
        public string Name { get; private set; }
        public decimal Price { get; private set; }
    
        private Product() {} // For ORM/factory
    
        public Product(Guid id, string name, decimal price)
        {
            if (id == Guid.Empty) throw new ArgumentException("Id cannot be empty.", nameof(id));
            // ... more validation logic ...
            Id = id; Name = name; Price = price;
        }
        public void UpdatePrice(decimal newPrice) { /* ... validation & logic ... */ Price = newPrice; }
    }
  • Value Objects: Immutable objects defined by their attributes, not an ID.
  • Aggregates: Clusters of domain objects ensuring consistency within a boundary, with an Aggregate Root entity.
  • Domain Services: Business logic that doesn't naturally fit within a single entity.
  • Interfaces: Abstractions defined by the Domain layer.
    • Repository Interfaces: Contracts for data persistence operations (e.g., IProductRepository).
      // Domain/Interfaces/IProductRepository.cs
      public interface IProductRepository
      {
          Task<Product?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
          Task<IEnumerable<Product>> GetAllAsync(CancellationToken cancellationToken = default);
          Task AddAsync(Product product, CancellationToken cancellationToken = default);
          Task UpdateAsync(Product product, CancellationToken cancellationToken = default);
          Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
      }
    • Other domain-specific service interfaces.
  • Domain Events: Represent something significant that happened in the domain. Used for side effects and decoupling.
  • Custom Domain Exceptions: Specific exceptions to signal violations of domain rules (e.g., `InsufficientStockException`).
  • Specifications: (e.g., for defining complex query criteria in a reusable way).
  • Domain-Specific Validators/Guards: For validating entities against business rules and ensuring invariants.
  • Enums: Domain-specific enumerations.
  • (Event Handlers - Domain-Level, if applicable): Handlers for domain events orchestrating logic *within* the domain.
Application Layer

Purpose: Contains application-specific business logic. It orchestrates use cases by interacting with the Domain layer and coordinating with the Infrastructure layer through interfaces.

Key Components
Key Components:
  • Use Cases/Interactors:
    • Commands & Command Handlers: Encapsulate operations that change the system's state.
      // Application/Products/Commands/CreateProductCommand.cs
      public record CreateProductCommand(string Name, decimal Price) : IRequest<Guid>;
      
      // Application/Products/Handlers/CreateProductCommandHandler.cs
      public class CreateProductCommandHandler : IRequestHandler<CreateProductCommand, Guid>
      {
          private readonly IProductRepository _productRepository;
          private readonly IUnitOfWork _unitOfWork;
      
          public CreateProductCommandHandler(IProductRepository productRepository, IUnitOfWork unitOfWork)
          {
              _productRepository = productRepository; _unitOfWork = unitOfWork;
          }
      
          public async Task<Guid> Handle(CreateProductCommand request, CancellationToken cancellationToken)
          {
              var product = new Product(Guid.NewGuid(), request.Name, request.Price);
              await _productRepository.AddAsync(product, cancellationToken);
              await _unitOfWork.SaveChangesAsync(cancellationToken); // Explicitly save changes
              return product.Id;
          }
      }
    • Queries & Query Handlers: Encapsulate operations that retrieve data without altering state.
  • Data Transfer Objects (DTOs): For transferring data between layers.
  • Interfaces for Infrastructure Services: Abstractions for infrastructure concerns (e.g., `IEmailSender`, `IDateTimeProvider`, `IFileStorage`).
  • Validation Logic: For input data (e.g., using FluentValidation, often as MediatR pipeline behaviors).
  • CQRS Pattern Support: Tools like MediatR are commonly used here to separate commands and queries.
  • Application Exceptions: For errors specific to application logic (e.g., `ValidationException`, `NotFoundException`).
  • Pipeline Behaviors (e.g., for MediatR): For cross-cutting concerns like validation, logging, or caching applied to use cases.
Infrastructure Layer

Purpose: Handles all external concerns and technical details like databases, file systems, network calls, and third-party services. It implements the interfaces defined in the Application and Domain layers.

Key Components
Key Components:
  • Data Access/Persistence Implementations:
    • Repositories: Concrete implementations of repository interfaces (e.g., using Entity Framework Core).
      // Infrastructure/Persistence/Repositories/ProductRepository.cs
      public class ProductRepository : IProductRepository
      {
          private readonly ApplicationDbContext _dbContext;
          public ProductRepository(ApplicationDbContext dbContext) {_dbContext = dbContext;}
          // ... implementations of IProductRepository methods using _dbContext ...
          public async Task AddAsync(Product product, CancellationToken ct) => 
              await _dbContext.Products.AddAsync(product, ct);
          // ... etc.
      }
    • DbContext: EF Core `DbContext` class for database interaction.
      // Infrastructure/Persistence/ApplicationDbContext.cs (EF Core example)
      public class ApplicationDbContext : DbContext, IUnitOfWork // Explicitly implementing IUnitOfWork
      {
          public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { }
          public DbSet<Product> Products { get; set; }
          // ... other DbSets
      
          protected override void OnModelCreating(ModelBuilder modelBuilder)
          {
              modelBuilder.ApplyConfigurationsFromAssembly(typeof(ApplicationDbContext).Assembly);
              base.OnModelCreating(modelBuilder);
          }
      
          public async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
          {
              return await base.SaveChangesAsync(cancellationToken);
          }
      }
    • Database migrations.
  • External Service Integrations:
    • Clients for payment gateways, email services, SMS services, and third-party APIs.
    • Cloud service accessors (e.g., Azure Storage, AWS S3).
    • API clients for consuming other services.
  • Caching Implementations: (e.g., Redis, In-Memory).
  • Identity Service Implementations: User authentication and authorization.
  • File System Accessors/Implementations.
  • Clock/DateTime Service Implementations.
  • Logging Service Implementations.
  • Other Infrastructure Services: (e.g., concrete implementations of any other interfaces defined by the Application layer for infrastructure concerns).
Presentation Layer (Web API)

Purpose: Handles user interaction (HTTP requests/responses for an API). It translates user input into commands/queries for the Application layer and presents the results.

Key Components
Key Components:
  • Controllers/API Endpoints: Receive HTTP requests, perform minimal validation/mapping, and delegate to Application layer.
    // Presentation/Controllers/ProductsController.cs
    [ApiController]
    [Route("api/[controller]")]
    public class ProductsController : ControllerBase
    {
        private readonly ISender _mediator; // Using MediatR's ISender
    
        public ProductsController(ISender mediator) {_mediator = mediator;}
    
        [HttpPost]
        public async Task<IActionResult> CreateProduct([FromBody] CreateProductCommand command)
        {
            var productId = await _mediator.Send(command);
            return CreatedAtAction(nameof(GetProductById), new { id = productId }, new { id = productId });
        }
    
        [HttpGet("{id}")]
        public async Task<IActionResult> GetProductById(Guid id)
        {
            // var query = new GetProductByIdQuery(id); // Assuming a GetProductByIdQuery exists
            // var productDto = await _mediator.Send(query);
            // if (productDto == null) return NotFound();
            // return Ok(productDto);
            return Ok($"Product with ID {id} retrieved (example)."); // Simplified for cheatsheet
        }
    }
  • Middleware: For cross-cutting concerns like global error handling, authentication, request logging.
  • Dependency Injection (DI) Setup / Composition Root: Configuration of services and their dependencies (typically in `Program.cs`). This is the "Composition Root".
  • API Models/ViewModels: Models specific to API requests/responses. Often mapped to/from Application layer DTOs.
  • API Versioning, OpenAPI/Swagger Configuration.
  • Filters (Action Filters, Exception Filters, etc.): For cross-cutting concerns specific to API request processing.
  • Model Binders: Custom logic for binding request data to models.
  • (Other UI-specific services if not strictly a Web API, e.g., for MVC: ViewModels, Tag Helpers - though less relevant for a pure Web API cheatsheet).

How a Request Flows Through a .NET Clean Architecture Solution

02 / runtime pathOne product request, from HTTP to persistence and back
  1. 01 Β· INPresentationBind POST /api/products
  2. 02 Β· USE CASEApplicationValidate and handle command
  3. 03 Β· RULESDomainEnforce Product invariants
  4. 04 Β· ADAPTERInfrastructurePersist via EF Core
  5. 05 Β· OUTPresentationReturn 201 Created

The Application layer calls an interface it owns. Dependency injection supplies the Infrastructure implementation at runtime.

A Single HTTP Request, End to End

The clearest way to understand Clean Architecture in .NET is to trace one request through the layers. Dependencies point inward, but control flows outward then back: the outer Web API calls into the Application layer, which reaches the Infrastructure layer only through interfaces the inner layers own. Consider POST /api/products in an ASP.NET Core Web API:

  1. Presentation (Web API): a thin controller or minimal-API endpoint model-binds the request, then dispatches a CreateProductCommand through MediatR. It contains no business logic.
  2. Application: a CreateProductCommandHandler runs the use case. A MediatR pipeline behavior validates the command with FluentValidation first. The handler talks to persistence only through an interface such as IProductRepository β€” an abstraction defined in the inner layers, never a concrete DbContext.
  3. Domain: the handler constructs or mutates a Product entity, which enforces its own invariants (a negative price throws). Enterprise rules live here and depend on nothing else in the solution.
  4. Infrastructure: at runtime the DI container has injected the EF Core-backed ProductRepository that implements IProductRepository. It translates the operation to SQL and persists via ApplicationDbContext. The Application layer never knows EF Core is behind the interface.
  5. Back out: the handler returns a DTO; the endpoint maps it to an HTTP 201 Created. Global exception-handling middleware in Presentation turns domain/application exceptions into consistent problem responses.
Where EF Core, CQRS, MediatR, and DI actually sit
The one-line placement map:
  • The Dependency Rule: source-code dependencies only point inward. Domain depends on nothing; Application depends on Domain; Infrastructure and Presentation depend inward on Application/Domain. Inner layers define interfaces; outer layers implement them (Dependency Inversion).
  • EF Core: lives entirely in Infrastructure (DbContext, migrations, entity configurations, repository implementations). The Domain and Application layers reference only interfaces, so the database can be swapped without touching business rules.
  • CQRS: lives in Application as separate command and query models. Commands change state and return little; queries read and can bypass the domain model for efficient read DTOs.
  • MediatR: the in-process mediator in Application that routes commands/queries to handlers and hosts cross-cutting pipeline behaviors (validation, logging, transactions). It decouples the sender (a controller) from the handler.
  • Dependency Injection: wired in the Presentation layer's Program.cs β€” the Composition Root. It is the one place allowed to know every concrete type, binding IProductRepository to ProductRepository and so on.
Litmus test: if adding a database column forces a change in your Domain or Application project, a dependency is pointing the wrong way. Only Infrastructure should care.

Project Structure & Visual Studio Setup

Typical Solution Structure

A common way to organize projects in a .NET solution reflecting Clean Architecture:


YourProjectSolution.sln
β”œβ”€β”€ src
β”‚   β”œβ”€β”€ YourProject.Domain.csproj (.NET Class Library)
β”‚   β”‚   β”œβ”€β”€ Entities/
β”‚   β”‚   β”œβ”€β”€ Aggregates/
β”‚   β”‚   β”œβ”€β”€ Enums/
β”‚   β”‚   β”œβ”€β”€ Events/
β”‚   β”‚   β”œβ”€β”€ Exceptions/
β”‚   β”‚   β”œβ”€β”€ Interfaces/ (e.g., IProductRepository.cs)
β”‚   β”‚   β”œβ”€β”€ Specifications/
β”‚   β”‚   β”œβ”€β”€ Validators/Guards/
β”‚   β”‚   └── ValueObjects/
β”‚   β”‚
β”‚   β”œβ”€β”€ YourProject.Application.csproj (.NET Class Library)
β”‚   β”‚   β”œβ”€β”€ Features/ (Organized by feature, e.g., Products, Orders)
β”‚   β”‚   β”‚   β”œβ”€β”€ Products/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ Commands/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ Queries/
β”‚   β”‚   β”‚   β”‚   └── DTOs/
β”‚   β”‚   β”œβ”€β”€ Common/
β”‚   β”‚   β”‚   β”œβ”€β”€ Interfaces/ (e.g., IEmailSender.cs, IDateTimeProvider.cs)
β”‚   β”‚   β”‚   β”œβ”€β”€ Behaviors/ (MediatR pipeline behaviors)
β”‚   β”‚   β”‚   └── Mappings/ (AutoMapper profiles if used)
β”‚   β”‚   └── Exceptions/
β”‚   β”‚
β”‚   β”œβ”€β”€ YourProject.Infrastructure.csproj (.NET Class Library)
β”‚   β”‚   β”œβ”€β”€ Persistence/
β”‚   β”‚   β”‚   β”œβ”€β”€ DataContext/ (e.g., ApplicationDbContext.cs)
β”‚   β”‚   β”‚   β”œβ”€β”€ Repositories/ (e.g., ProductRepository.cs)
β”‚   β”‚   β”‚   β”œβ”€β”€ Migrations/
β”‚   β”‚   β”‚   └── Configurations/ (EF Core entity type configurations)
β”‚   β”‚   β”œβ”€β”€ Services/ (e.g., EmailSender.cs, DateTimeProvider.cs)
β”‚   β”‚   └── Identity/
β”‚   β”‚
β”‚   └── YourProject.WebApi.csproj (ASP.NET Core Web API Project)
β”‚       β”œβ”€β”€ Controllers/
β”‚       β”œβ”€β”€ Middleware/
β”‚       β”œβ”€β”€ Filters/
β”‚       β”œβ”€β”€ Extensions/ (Service registration extensions)
β”‚       β”œβ”€β”€ appsettings.json
β”‚       └── Program.cs (Composition Root)
β”‚
└── tests
    β”œβ”€β”€ YourProject.Domain.UnitTests.csproj
    β”œβ”€β”€ YourProject.Application.UnitTests.csproj
    β”œβ”€β”€ YourProject.Infrastructure.IntegrationTests.csproj
    └── YourProject.Presentation.IntegrationTests.csproj
                    
Visual Studio Setup Guide
Setting up the Projects in Visual Studio:
  1. Create the Solution and Presentation Layer (Web API):
    • Open Visual Studio.
    • Select "Create a new project".
    • Choose the "ASP.NET Core Web API" template. Click Next.
    • Name your project (e.g., `YourProject.WebApi`) and solution (e.g., `YourProjectSolution`). Click Next.
    • Choose your desired .NET framework version. Configure other settings as needed. Click Create.
    • This project will serve as your Presentation Layer.
  2. Add the Domain Layer:
    • In Solution Explorer, right-click on the Solution (`YourProjectSolution`).
    • Select Add > New Project...
    • Choose the "Class Library" template. Click Next.
    • Name the project `YourProject.Domain`. Click Next.
    • Choose the same .NET framework version as your Web API project. Click Create.
    • Delete the default `Class1.cs` file.
    • Create folders like `Entities`, `Interfaces`, `ValueObjects`, etc., as shown in the structure above.
    • Important: The Domain project should NOT have project references to any other layer project in this solution.
  3. Add the Application Layer:
    • Right-click on the Solution > Add > New Project...
    • Choose "Class Library". Name it `YourProject.Application`.
    • Ensure the .NET framework version matches. Click Create.
    • Delete `Class1.cs`.
    • Create folders like `Features`, `Common/Interfaces`, `DTOs`, etc.
    • Add Project Reference: Right-click on the `YourProject.Application` project > Add > Project Reference... > Check `YourProject.Domain` > OK.
  4. Add the Infrastructure Layer:
    • Right-click on the Solution > Add > New Project...
    • Choose "Class Library". Name it `YourProject.Infrastructure`.
    • Ensure the .NET framework version matches. Click Create.
    • Delete `Class1.cs`.
    • Create folders like `Persistence/DataContext`, `Repositories`, `Services`, etc.
    • Add Project Reference: Right-click on the `YourProject.Infrastructure` project > Add > Project Reference... > Check `YourProject.Application` > OK. (The Infrastructure layer implements interfaces defined in Application and Domain).
    • Install necessary NuGet packages here (e.g., `Microsoft.EntityFrameworkCore`, database providers, SDKs).
  5. Configure Presentation Layer Dependencies:
    • The `YourProject.WebApi` (Presentation) project orchestrates the application.
    • Add Project References: Right-click on the `YourProject.WebApi` project > Add > Project Reference... > Check `YourProject.Application` and `YourProject.Infrastructure` > OK.
    • This allows the Web API to send commands/queries to the Application layer and to register services (dependency injection) from the Infrastructure layer in `Program.cs`.
  6. Set Startup Project:
    • Right-click on the `YourProject.WebApi` project in Solution Explorer and select "Set as Startup Project".

Dependency Flow Reminder:
Presentation (WebApi) β†’ references Application & Infrastructure
Infrastructure β†’ references Application (to implement its interfaces and use its DTOs/Domain types)
Application β†’ references Domain
Domain β†’ references (No other project dependencies within the solution)

Scaffold the Solution with the .NET CLI

Copy-Paste Setup (dotnet CLI)

The same four-project layout, built from the command line. The add reference order is what enforces the Dependency Rule: references only ever point inward.

# 1. Solution + projects (Domain and Application are class libraries)
dotnet new sln -n Commerce
dotnet new classlib -n Commerce.Domain
dotnet new classlib -n Commerce.Application
dotnet new classlib -n Commerce.Infrastructure
dotnet new webapi   -n Commerce.WebApi

dotnet sln add Commerce.Domain Commerce.Application Commerce.Infrastructure Commerce.WebApi

# 2. Wire references INWARD only (this is the architecture)
dotnet add Commerce.Application  reference Commerce.Domain
dotnet add Commerce.Infrastructure reference Commerce.Application   # to implement its interfaces
dotnet add Commerce.WebApi       reference Commerce.Application Commerce.Infrastructure  # composition root
# NOTE: Commerce.Domain references nothing else in the solution.

# 3. Packages land in the layer that owns the concern
dotnet add Commerce.Infrastructure package Microsoft.EntityFrameworkCore.SqlServer
dotnet add Commerce.Infrastructure package Microsoft.EntityFrameworkCore.Design
dotnet add Commerce.Application  package MediatR            # in-process CQRS mediator (see licensing note below)
dotnet add Commerce.Application  package FluentValidation   # MIT-licensed

# 4. EF Core migration (run from the solution root; note the two project flags)
dotnet ef migrations add InitialCreate \
  --project Commerce.Infrastructure --startup-project Commerce.WebApi
dotnet ef database update \
  --project Commerce.Infrastructure --startup-project Commerce.WebApi
Why --project and --startup-project differ: the DbContext lives in Infrastructure (--project), but EF needs the app's DI configuration and connection string, which live in the Web API composition root (--startup-project). Pointing both at one project is the most common EF migration failure in a layered solution.

Best Practices & Considerations

Key Practices
  • Dependency Injection (DI): Absolutely crucial. Register dependencies in the Presentation layer's `Program.cs` (the Composition Root). Use constructor injection primarily.
  • MediatR: Widely used for implementing CQRS in the Application layer. It helps decouple command/query senders from their handlers and allows for cross-cutting concerns via pipeline behaviors. Licensing note (as of 2026): MediatR v13.0.0+ moved to a commercial model under LuckyPennySoftware. It remains free for individuals and organisations with under $5M annual revenue; larger enterprises require a paid license registered at mediatr.io. Latest stable: v14.2.0.
  • FluentValidation: A popular library for robust validation in the Application layer, often integrated with MediatR pipelines. Remains MIT-licensed and open source. Latest stable: v12.1.1.
  • AutoMapper (or similar): Useful for mapping between Entities, DTOs, and API Models. Define profiles in the Application layer or where the mapping is most relevant. Licensing note (as of 2026): AutoMapper v15+ also moved to a commercial model under LuckyPennySoftware with the same tiered free/paid structure (free under $5M revenue). Latest stable: v16.2.0. MIT-licensed alternatives such as Mapperly (source-generator based, latest v4.3.1) are widely adopted as free replacements.
  • Unit of Work (UoW) Pattern: Often implemented in the Infrastructure layer (e.g., within the `DbContext`). An `IUnitOfWork` interface can be defined in the Application layer to be consumed by command handlers.
  • Error Handling: Implement a global error handling middleware in the Presentation layer to catch exceptions and return consistent API error responses. Define custom exceptions in Domain and Application layers for specific business or application errors.
  • Configuration (Options Pattern): Use the Options pattern (IOptions<T>) for strongly-typed configuration, typically configured in the Presentation layer and injected where needed.
  • Async/Await: Use `async`/`await` thoroughly for I/O-bound operations. Prefer `async Task` over `async void` for most methods (except event handlers where `async void` is sometimes necessary).
  • Single Responsibility Principle (SRP): Apply SRP to classes and methods within each layer to improve cohesion and reduce coupling.
  • Lean Controllers: Controllers should be thin, primarily responsible for receiving HTTP requests, validating input (often with model binding), and delegating work to the Application layer (e.g., MediatR). Avoid business logic in controllers.
  • Avoid Leaking Abstractions: Do not expose `IQueryable` from repositories directly to the Application or Presentation layers, as this can lead to infrastructure concerns (like specific EF Core LINQ expressions) leaking outwards. Queries should be fully defined within the data access layer or use well-defined specification patterns.
  • Testing Strategy:
    • Domain Layer: Pure unit tests with no external dependencies.
    • Application Layer: Unit tests, mocking repository interfaces and other infrastructure dependencies.
    • Infrastructure Layer: Integration tests against a real (or test instance of) database or external services.
    • Presentation Layer: Integration tests (testing API endpoints, e.g., using `WebApplicationFactory`).

Common Mistakes & Anti-Patterns

Where Clean Architecture in .NET Goes Wrong

Most failed Clean Architecture projects are not too little structure but the wrong structure. These are the recurring mistakes and the fix for each:

  • Anemic domain, fat services: entities become bags of public setters while all logic sits in Application "services." Fix: push invariants into the Domain (private setters, behavior methods); the Application layer should orchestrate, not own business rules.
  • Referencing EF Core from Domain or Application: adding Microsoft.EntityFrameworkCore to an inner project, or decorating entities with EF attributes. Fix: keep the ORM in Infrastructure; configure mappings with IEntityTypeConfiguration, not attributes on domain types.
  • Leaking IQueryable out of repositories: returning IQueryable<T> pushes EF translation concerns into upper layers and breaks the abstraction. Fix: return materialized results or use the specification pattern; the query is fully defined in Infrastructure.
  • Interfaces in the wrong layer: defining IProductRepository in Infrastructure defeats dependency inversion. Fix: the abstraction belongs to the inner layer that consumes it (Domain or Application); Infrastructure only implements it.
  • DTOs bleeding into the Domain: reusing API request/response models as entities couples business rules to the wire format. Fix: map between DTOs and domain types at the Application boundary.
  • Over-engineering a CRUD app: four projects, MediatR, and repositories for a two-table admin tool is pure overhead. Fix: Clean Architecture pays off for complex, long-lived domains; a simple CRUD service can stay a single project.
  • Repository-over-DbContext that adds nothing: a generic Repository<T> wrapping DbSet<T> 1:1 just hides a capable API. Fix: use repositories for real aggregate boundaries, or query DbContext directly in query handlers.
  • Business logic in controllers: validation, branching, and orchestration in the Presentation layer. Fix: keep controllers thin β€” bind, dispatch a command/query, map the result.
  • Circular or lateral project references: Infrastructure referencing Presentation, or two feature projects referencing each other. Fix: enforce the inward-only direction with an architecture test (NetArchTest or ArchUnitNET) in CI so a bad reference fails the build.