Clean Architecture in .NET – Layered with Purpose

When a .NET code-base starts to feel like a ball of mud—controllers talking to SQL directly, business rules leaking into UI helpers—maintenance slows to a crawl. Clean Architecture is a disciplined way to organise the mess into clear, testable layers that can grow without tripping over themselves.

Real-Life Analogy: The Onion Kitchen

Picture an onion-shaped commercial kitchen. At the very centre, a vault stores the head chef’s secret recipes. Around that are prep stations, then serving counters, and finally delivery vans parked outside. Information flows inwards only by contracts—the vault never knows (or cares) whether the final dish is plated in fine dining or boxed for takeaway.

Onion diagram showing Clean Architecture layers
The onion: stable core recipes (Entities) surrounded by rings of higher-level concerns.

Entities — Core Rules

Entities are the secret recipes: plain C# classes containing business logic that must never break. No frameworks, no data access, no web references.

// Domain entity
public class Invoice
{
    public Guid Id { get; init; }
    public Money Total { get; private set; }

    public void AddLineItem(Money amount) => Total += amount;
}

Use Cases — Application Layer

Use-case classes (sometimes called Services or Interactors) orchestrate entities to fulfil a specific business scenario. They depend only on abstractions defined in the core.

// Application service
public class CreateInvoice
{
    private readonly IInvoiceRepository _repo;
    public CreateInvoice(IInvoiceRepository repo) => _repo = repo;

    public async Task<Guid> ExecuteAsync(CreateInvoiceCommand cmd)
    {
        var invoice = new Invoice();
        foreach (var line in cmd.Lines) invoice.AddLineItem(line.Amount);
        await _repo.SaveAsync(invoice);
        return invoice.Id;
    }
}

Interface Adapters

Here we translate the outside world into something the inner layers understand:

  • Controllers / gRPC endpoints → map HTTP or gRPC calls to use-case commands.
  • Repositories → wrap EF Core, Dapper, or SQL into the IInvoiceRepository abstraction.
  • Mappers / DTOs → keep transport objects from contaminating entities.

Frameworks & Drivers

The outermost ring is where concrete tech lives: ASP.NET Core, EF Core, SQL Server, Message-Queues, UI frameworks. Swap them at will—the inner rings remain untouched.

Typical Folder Layout

src/
 ├─ Domain/           # Entities & value objects
 ├─ Application/      # Use cases & abstractions
 ├─ Infrastructure/   # EF Core, external APIs, File IO
 ├─ Web/              # ASP.NET Core controllers
 └─ Tests/            # Unit & integration tests

Common Pitfalls

  • Skipping Abstractions: Letting the Application project reference EF Core types breaks isolation.
  • Too Many Layers: Four rings are enough—don’t introduce gratuitous “services” layers.
  • God Entities: Keep entities small; aggregate roots coordinate, they don’t monopolise.

Final Thoughts

Clean Architecture is not dogma; it is a guard-rail. By honouring inward-only dependencies, you gain testability, swap-ability, and the confidence to evolve features without fear. In my experience, that trade-off easily pays for itself once a project crosses a few thousand lines.