Application Architecture in C#
Architecture is not a folder layout, nor the list of projects in a solution. It is a set of constraints on the direction of dependencies that decides what your next change will cost: whether you can swap the database engine without touching business rules; whether you can add a field to an entity without breaking the API contract; whether you can test a use case without booting a database. Everything else follows.
Hence an honest position on the three most-quoted things in this topic. Clean/Onion is the rule that dependencies point inward, not four projects with particular names: a solution with Domain, Application, Infrastructure and Web projects in which Domain references Microsoft.EntityFrameworkCore is not Clean architecture in any sense. The Repository pattern on top of EF Core is usually redundant: DbSet<T> is already a repository and DbContext is already a Unit of Work, so a pass-through wrapper abstracts nothing and merely hides IQueryable. A DTO exists to decouple the wire contract from the domain model: return entities from your controllers and you have made the database schema a public API — which is how you get accidental breaking changes and over-posting. Layer by layer, below.
Topic map
- Application layers — presentation, application, domain, infrastructure; Clean/Onion as the rule that dependencies point inward.
- DTOs and domain entities — a flat boundary contract versus an entity with identity and invariants; what leaking an entity into the API costs.
- Dependency inversion —
DIP, constructor injection, the composition root, and why the interface must belong to the domain. - The Repository pattern — when a wrapper over EF Core earns its keep, and when it is one more layer on top of an existing repository and Unit of Work.
Common Mistakes and Traps
| Mistake | Consequence |
|---|---|
Referencing DbContext or ASP.NET Core types from the domain project | The domain depends on infrastructure — swapping the ORM or the host drags business rules along; the dependency rule is broken whatever the projects are called |
| Putting business rules into controllers | The domain becomes an anaemic bag of properties and the rules get duplicated in every endpoint |
| Treating layers as a folder layout | The dependency rule is never enforced — you have "layers" and the same coupling |
| Declaring the repository interface in Infrastructure | The domain depends on infrastructure again: the dependency is renamed, not inverted |
Assuming any use of a DI container satisfies DIP | A service locator inside a method (provider.GetService<T>()) hides the dependency and inverts nothing |
Treating DIP as a testability trick | Testability is a consequence; the rule is about the direction of dependencies |
Wrapping DbSet<T> in a pass-through repository | One more layer for nothing: DbSet<T> is already a repository, DbContext is already a Unit of Work |
Returning IQueryable from a repository | The very technology the repository was meant to hide leaks out — the caller now dictates the SQL |
| Returning EF Core entities from controllers | The database schema becomes the API contract: a column migration is a breaking change, and extra fields get bound (over-posting) |
Putting business rules and invariants into a DTO | Invariants move out of the domain to the boundary and stop holding for every other caller |
Interview relevance
Architecture questions are the middle/senior watershed on a C# interview. A junior lists the layers. A middle explains why you inject through the constructor. A senior names the rule — dependencies point inward, toward the domain — and, more tellingly, is honest about where a pattern is not needed: the ability to say "a Repository over EF Core is redundant here, because…" is what separates an engineer from someone who has read an article.
Typical checks:
- The layers of a typical ASP.NET Core solution and the "a layer only calls the one below it" rule.
- That Clean/Onion is the inward direction of dependencies, not a set of project names, and that the domain references nothing.
- How constructor injection realizes
DIP, and why the interface is declared by the consumer (the domain), not by the implementation. - What a composition root is and how it differs from a service locator.
- Whether
Repositoryover EF Core is justified — and whether you can name the two cases where it is. - What a
DTOis for, and what happens when an entity leaks into the API contract.
Common wrong answer: "We did Clean architecture — we have Domain, Application, Infrastructure and Web projects." That describes folders. The follow-up — "and what does Domain reference?" — settles it: if the answer contains DbContext, there is no architecture, only a layered layout with its dependencies pointing outward.