Home About Skills Products Work
Projects Services Experience
Learn
Tutorials Courses Blogs Resources
Contact
Blog · .NET

Clean Architecture in ASP.NET Core

Clean Architecture in ASP.NET Core

Clean Architecture organizes an ASP.NET Core application so business logic has zero dependency on frameworks, databases, or UI — meaning your core rules can be tested, and even reused, completely independent of how the app is currently built or deployed.

The Dependency Rule

Clean Architecture is built around one rule: dependencies only point inward. The innermost layer (Domain) knows nothing about anything outside it. Each layer out from there can depend on layers further in, never the reverse.

Domain          ← no dependencies on anything
Application     ← depends on Domain only
Infrastructure  ← depends on Application + Domain (implements interfaces they define)
Web (API)       ← depends on all of the above, wires everything together

Domain Layer: Pure Business Rules

// Domain/Entities/Order.cs
public class Order
{
    public int Id { get; private set; }
    public OrderStatus Status { get; private set; }

    public void MarkAsShipped()
    {
        if (Status != OrderStatus.Paid)
            throw new InvalidOperationException("Only paid orders can ship.");
        Status = OrderStatus.Shipped;
    }
}

No Entity Framework attributes, no DbContext reference, no HTTP concepts anywhere in this file — it's plain C# that could compile with zero external packages.

Application Layer: Use Cases and Interfaces

// Application/Interfaces/IOrderRepository.cs
public interface IOrderRepository
{
    Task<Order?> GetByIdAsync(int id);
    Task SaveAsync(Order order);
}

// Application/UseCases/ShipOrder.cs
public class ShipOrderHandler
{
    private readonly IOrderRepository _repo;
    public ShipOrderHandler(IOrderRepository repo) => _repo = repo;

    public async Task Handle(int orderId)
    {
        var order = await _repo.GetByIdAsync(orderId)
            ?? throw new NotFoundException();
        order.MarkAsShipped();
        await _repo.SaveAsync(order);
    }
}

The Application layer defines what a repository must do (IOrderRepository) without knowing HOW — that's Infrastructure's job.

Infrastructure: The Implementation Details

// Infrastructure/Repositories/EfOrderRepository.cs
public class EfOrderRepository : IOrderRepository
{
    private readonly AppDbContext _db;
    public EfOrderRepository(AppDbContext db) => _db = db;

    public Task<Order?> GetByIdAsync(int id) =>
        _db.Orders.FindAsync(id).AsTask();

    public Task SaveAsync(Order order) =>
        _db.SaveChangesAsync();
}

Swap Entity Framework for Dapper, or MySQL for PostgreSQL, and only this file changes — the Domain and Application layers never notice.

Wiring It Together in Program.cs

builder.Services.AddScoped<IOrderRepository, EfOrderRepository>();
builder.Services.AddScoped<ShipOrderHandler>();

The real payoff: ShipOrderHandler's tests inject a fake, in-memory IOrderRepository — no database, no HTTP server, no test infrastructure at all, just fast, isolated unit tests of your actual business rule.

Building Your First REST API with ASP.NET Core

Building Your First REST API with ASP.NET Core

Minimal APIs, Entity Framework Core, and FluentValidation — a real, working ASP.NET Core API without controller-class ceremony.

ASP.NET Core Web API Authentication with JWT

ASP.NET Core Web API Authentication with JWT

A complete JWT authentication setup for ASP.NET Core — issuing tokens, protecting endpoints, and role-based authorization.

Entity Framework Core Migrations: A Practical Guide

Creating, applying, and safely rolling back EF Core migrations — the .NET equivalent of Laravel migrations, and where the workflow differs.

Esc