Clean Architecture in .NET: What Earns Its Keep
Four projects, a pile of interfaces, and a mapping layer nobody enjoys. Here is the part of clean architecture I keep, and the part I drop.
Almost every .NET codebase I have inherited in the last five years has the same four folders: Domain, Application, Infrastructure, Web. Sometimes that structure is carrying real weight. More often it is ceremony — an interface per class, a DTO per entity, and a mapper nobody wants to touch.
What I keep
The dependency rule. Domain knows nothing about the outside world; infrastructure depends inward, never the reverse. That single constraint is what lets you swap a database, add a second delivery mechanism, or test business rules without spinning up a server.
// Application layer: the contract lives with the use case
public interface IProjectRepository
{
Task<Project?> GetBySlugAsync(string slug, CancellationToken ct);
}
// Infrastructure layer: EF Core knows about Application, not the reverse
public sealed class ProjectRepository(AppDbContext db) : IProjectRepository
{
public Task<Project?> GetBySlugAsync(string slug, CancellationToken ct) =>
db.Projects.AsNoTracking().FirstOrDefaultAsync(p => p.Slug == slug, ct);
}What I drop
- An interface for every service, whether or not a second implementation will ever exist
- DTOs that are field-for-field copies of the entity, mapped by hand in both directions
- A generic repository over an ORM that is already a repository
- Layer-crossing abstractions added because a diagram said so, not because a change was painful
The honest test is this: point at an abstraction and name the change it is protecting you from. If nobody in the room can name one, it is not architecture — it is indirection with good PR.
- #.NET Core
- #Clean Architecture
- #Patterns