DEV Community

Cover image for ASP.NET Core MVC project structure: a practical layered architecture
GANESH KUMAR
GANESH KUMAR

Posted on

ASP.NET Core MVC project structure: a practical layered architecture

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
Enter fullscreen mode Exit fullscreen mode

References only point "down":

Web  →  Services  →  Data  →  Entities
  ↘        ↓          ↓
           Shared  ←──┘
Enter fullscreen mode Exit fullscreen mode

Entities and Shared depend on nothing in the solution. Web never touches the DbContext directly; it goes through Services.

Five projects: Web references Services, Services references Data, Data references Entities, and all can use Shared

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));
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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. Services plays 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 IPaymentGateway or IEmailSender. 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)