aspnet-core-clean-architecture-capas

Clean Architecture: How to Organize a Project in Layers

  • 5 min

The Clean Architecture is a way to separate business rules, use cases, and infrastructure by making dependencies point toward the core of the application.

When you start a small project, putting everything in a controller or in Program.cs is tempting. It’s quick, it’s easy, and it works. As the project grows, that “ease” can end up with business logic mixed with SQL, duplicated validations, and hard-to-write tests.

One way to avoid this is to use a layered architecture. Clean Architecture, popularized by Robert C. Martin (Uncle Bob), is one of the most well-known proposals.

Let’s see how to divide the solution into four projects (.csproj) to achieve more decoupled and easily testable code.

Folder Structure in Visual Studio

Your solution explorer should look like this:

📂 MySolution.sln / ├── 📂 src / │ ├── 📜 MyApp.Domain # (Class Library) │ ├── 📜 MyApp.Application # (Class Library) │ ├── 📜 MyApp.Infrastructure # (Class Library) │ └── 📜 MyApp.Api # (ASP.NET Core Web API) └── 📂 tests / └── 📜 MyApp.UnitTests

Dependencies

Before looking at the layers, you need to understand the rule that governs everything: The Dependency Rule.

Source code dependencies can only point inward.

Inner layers (the core) must not know anything about outer layers.

  • Your Database (Outer) depends on your Domain (Inner), not the other way around.
  • Your Domain doesn’t know if you are using SQL Server, Mongo, or an Excel file.
  • Your Domain doesn’t know if you are in a Web API, a Console App, or an Azure Function.

The Four Layers of a .NET Solution

In a standard Visual Studio solution, we will create 4 separate projects (Class Libraries):

Domain

It is the center of the universe. The pure logic of your business lives here.

  • What does it contain?: Entities (User, Product), Value Objects, Enums, Custom Exceptions, and pure Domain Logic.
  • Dependencies: NONE. It doesn’t depend on any other project. It has no references to Entity Framework, HTTP, or anything. It’s pure C#.
// Project: MyApp.Domain
public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }

    // Pure Logic: A product cannot have a negative price
    public void UpdatePrice(decimal newPrice)
    {
        if (newPrice < 0) throw new DomainException("Invalid price");
        Price = newPrice;
    }
}
Copied!

Application

It wraps the Domain. Here we define what our application can do (Use Cases), but not how the data is saved.

  • What does it contain?: Repository Interfaces (IProductRepository), DTOs, Mappers, Validations (FluentValidation), and Application Services (CQRS or Managers).
  • Dependencies: Only on the Domain project.

Dependency Inversion: Here we define the Interfaces (IRepository), but we do NOT implement them. The Application layer says: “I need someone who knows how to save a product”, but it doesn’t care who or how it’s done.

// Project: MyApp.Application
public interface IProductRepository
{
    void Add(Product product); // Uses the Domain entity
}

public class CreateProductUseCase
{
    private readonly IProductRepository _repo; // Depends on the abstraction
    // ...
}
Copied!

Infrastructure

At this point, we get our hands dirty. We implement the interfaces defined by the Application layer.

  • What does it contain?: Entity Framework (DbContext), Migrations, Email Clients (SendGrid), File Clients (Azure Blob Storage).
  • Dependencies: Application (to see the interfaces) and Domain (to see the entities).
  • NuGet Packages: At this point, you install Microsoft.EntityFrameworkCore.SqlServer.
// Project: MyApp.Infrastructure
public class SqlProductRepository : IProductRepository
{
    private readonly ApplicationDbContext _db;

    public void Add(Product product)
    {
        _db.Products.Add(product); // Actual implementation with EF Core
    }
}
Copied!

API / Presentation

It is the entry point for users. In our case, it is the Web API project.

  • What does it contain?: Controllers, Program.cs, Configuration (appsettings.json), Middlewares, and Filters.
  • Dependencies: Application (to call the use cases) and Infrastructure (to inject dependencies into the DI container).

How does it all connect?

Let’s see the journey of a request to “Create User”:

API: The UsersController receives a JSON. It converts it to a DTO (CreateUserDto).

API -> Application: The controller calls a service/use case in the Application layer (UserService.Create(dto)).

Application -> Domain: The service converts the DTO into a Domain Entity (new User(...)) and runs business validations.

Application -> Infrastructure: The service calls _repository.Add(user). Since it is an interface, it doesn’t know it’s SQL.

Infrastructure: At runtime, thanks to Dependency Injection, the EF Core code that saves to the database is executed.

  1. Infrastructure Independence: If you change SQL Server for another technology, most of the change stays within Infrastructure, although the model and queries might also need adjustments.
  2. Testability: You can test business logic (Domain) and use cases (Application) without needing a database, simply by “mocking” the interfaces.
  3. Maintainability: You know exactly where to look for each thing.
  1. Over-engineering: For a simple CRUD of 3 tables or a proof of concept, this is overkill.
  2. Boilerplate: You have to create many files and mappings (DTO -> Entity -> DTO) just to move data.