Architecture debates online tend to swing between two extremes: everything in one project, or Clean Architecture with a dozen projects and an interface for every class. For a real e-commerce app I wanted something in between that a new developer could understand in ten minutes.
My solution, eCommerceMVCCore, has five projects. Here's what each does, which way the references point, and the trade-offs.
The five projects
eCommerce.Web MVC controllers, views, admin dashboard, REST API
eCommerce.Services business logic: pricing, orders, payments, email
eCommerce.Data EF Core DbContext, migrations, configurations
eCommerce.Entities domain classes: Product, Order, Category, PromoCode
eCommerce.Shared helpers, constants, localization keys
References only point "down":
Web → Services → Data → Entities
↘ ↓ ↓
Shared ←──┘
Entities and Shared depend on nothing in the solution. Web never touches the DbContext directly; it goes through Services.
Why controllers don't use the DbContext
This is the rule that pays off most. When the website's checkout and the mobile API's checkout both call IOrderService.PlaceOrderAsync, the rules for prices, stock and promo codes exist exactly once:
public class CheckoutController(IOrderService orders) : Controller
{
[HttpPost]
public async Task<IActionResult> PlaceOrder(CheckoutForm form)
{
var result = await orders.PlaceOrderAsync(User.GetUserId(), form.ToRequest());
return result.Succeeded
? RedirectToAction("Confirmation", new { id = result.OrderId })
: View("Index", form.WithError(result.Error));
}
}
The controller translates HTTP into a service call and back. Nothing else.
Why I didn't add a repository layer
EF Core's DbContext is already a unit of work, and each DbSet is already a repository. Wrapping them in IProductRepository with GetById, Add and Update mostly adds code, and it tends to hide the features that make EF Core good: projections, Include, ExecuteUpdateAsync.
Services use the DbContext directly. When a query is reused in several places, it becomes an extension method on IQueryable<Product>, for example .Published() or .InCategory(id), not a repository class.
Entities stay plain
Entities are plain classes with no EF attributes. Mapping lives in Data, in IEntityTypeConfiguration<T> classes:
public class ProductConfiguration : IEntityTypeConfiguration<Product>
{
public void Configure(EntityTypeBuilder<Product> b)
{
b.Property(p => p.Price).HasPrecision(18, 2);
b.HasIndex(p => p.Sku).IsUnique();
}
}
That keeps Entities free of database concerns, which matters when one model has to work on SQL Server, PostgreSQL, MySQL and SQLite.
View models live in Web
Views never get entities directly. A ProductDetailsViewModel contains exactly what the page shows. That avoids accidentally rendering fields you didn't mean to, and stops model binding from writing to properties like Price or IsAdmin that a form should never set (the over-posting problem).
Browse the structure: the eCommerceMVCCore source code is 100% C# and Razor, organised exactly like this.
What I deliberately left out
- MediatR and CQRS. Useful in big systems, but for this app it would double the number of classes without changing behaviour.
-
A separate Application and Domain layer.
Servicesplays both roles. If it grows too big, it can be split by feature later. -
Interfaces for everything. Services that need to be swapped or mocked get one, like
IPaymentGatewayorIEmailSender. Plain helpers don't.
Would I structure it differently next time?
Maybe by feature inside each project (Orders/, Catalog/, Payments/) rather than by type (Controllers/, Services/). The project boundaries, though, have held up well through a full .NET 10 upgrade.
FAQ
Do I need the repository pattern with EF Core?
Usually not. DbContext is already a unit of work and each DbSet is a repository. Add your own abstraction only where you really need to swap the data source or hide complex queries.
What's the difference between Clean Architecture and a layered architecture?
Clean Architecture inverts the dependencies, so the core has no reference to infrastructure. A layered architecture lets references point down to the data layer. It's simpler, and fine for most data-driven web apps.
How should I organise an ASP.NET Core solution?
Split by responsibility into a few projects, and organise by feature inside them. eCommerceMVCCore uses five projects: Web, Services, Data, Entities and Shared.
Get the source code
That clean five-project solution, 100% source code, is eCommerceMVCCore. Explore the ASP.NET Core 10 e-commerce platform.
How do you structure your ASP.NET Core apps? Repository layer or not? Let me know in the comments.

Top comments (0)