Clean Architecture for a .NET SaaS - what we keep, and what we skip
A pragmatic take on Clean Architecture in ASP.NET Core - the layers, MediatR commands and queries, validation pipelines, and why we don’t wrap EF Core in a repository.
Invoke Technologies · · 2 min read
“Clean Architecture” can mean anything from a tidy four-project solution to a maze of interfaces that exist only to satisfy a diagram. Here’s the version we actually use when we build SaaS products on ASP.NET Core, and the reasoning behind each choice.
Four layers, dependencies pointing inward
- Domain - entities and business rules. No references to web frameworks or databases.
- Application - use cases: commands, queries, validation, and the interfaces the outside world must implement.
- Infrastructure - EF Core, identity, payment and email providers. Implements Application’s interfaces.
- API - controllers, middleware, and composition. As thin as we can make it.
The payoff is testability. Business rules in Domain and Application can be unit tested without spinning up a web server or a database, and swapping an email provider touches exactly one project.
Commands and queries with MediatR
Each feature is a small request object and a handler:
public record CreateCampaignCommand(Guid OrganizationId, string Name, decimal Goal)
: IRequest<Guid>;
Controllers just send the request. That keeps them trivially thin, and it means a new feature is a new folder rather than another method bolted onto a growing “service” class.
Validation as a pipeline behavior
FluentValidation validators are discovered automatically and run in a MediatR pipeline behavior before any handler executes. Every request is validated the same way, and handlers can assume their input is well-formed.
What we skip: a repository on top of EF Core
DbContext already is a unit of work, and DbSet<T> already is a repository. Wrapping them in
another abstraction usually just hides useful features like projections and Include. Instead, the
Application layer depends on a small interface that exposes the DbSet<T> properties it needs -
enough to keep Infrastructure swappable in tests, without the ceremony.
Model the data for next year, not just this sprint
The most valuable architecture work is often in the data model. On a recent fundraising platform, we separated a person’s identity from their participation in a particular campaign, and a generic payment from the thing it paid for - before any real data existed. Both changes cost an afternoon then; retrofitting them later would have meant a risky migration of production data.
The takeaway
Use the layers to protect your business rules, keep the ceremony proportional to the problem, and write down why you made each call. Future you - and everyone who works on the code after you - will thank you.