Structuring Clean Architecture in ASP.NET Core Web APIs
Tightly coupling database frameworks like Entity Framework Core directly to your API controllers makes unit testing, maintainability, and future framework upgrades extremely difficult. Clean Architecture ensures that core business rules remain isolated at the center, with external dependencies pointing inward.
In this post, we will walk through structuring clean layer boundaries and implementing repository interface abstractions with Dependency Injection in ASP.NET Core.
1. The Four Core Layers
- Domain: Contains core entities, value objects, domain logic, and exceptions. Has zero external dependencies.
- Application: Houses business use cases, DTOs, service interfaces, and CQRS handlers. Depends only on the Domain layer.
- Infrastructure: Implements data persistence (EF Core, Dapper), file storage, and third-party API clients.
- API / Presentation: Contains REST Controllers, Middlewares, and Program.cs configuration. Entry point of the application.
2. Interface Abstractions in the Application Layer
Define interfaces for data access inside the Application layer so business logic relies on abstractions rather than concrete EF Core DbContext instances.
// IOrderRepository.cs - Application Layer Interface
namespace Application.Interfaces;
public interface IOrderRepository
{
Task<Order?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<IReadOnlyList<Order>> GetByCustomerIdAsync(Guid customerId, CancellationToken cancellationToken = default);
Task AddAsync(Order order, CancellationToken cancellationToken = default);
Task UpdateAsync(Order order, CancellationToken cancellationToken = default);
}
3. Registering Dependencies in Program.cs
Keep your API entry point clean by organizing service registrations into extension methods or injecting them clearly during startup.
// Program.cs - API Layer
var builder = WebApplication.CreateBuilder(args);
// Register Infrastructure & Application Services
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IOrderService, OrderService>();
// Configure Entity Framework Core with SQL Server
builder.Services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"),
sqlOptions => sqlOptions.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName)));
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Key Performance & Maintainability Benefits
- Testability: Mocking interfaces like
IOrderRepositoryin unit tests requires zero database setup or in-memory providers. - Framework Independence: Swapping EF Core for Dapper or switching databases requires changes only in the Infrastructure layer.
- Maintainable Codebase: Clear boundaries prevent business logic leakages into controllers or database queries inside presentation code.