Understanding nopCommerce Architecture: Layers, DI & Plugins

Plugin development 13 min read September 18, 2026

Understanding nopCommerce Architecture: Layers, DI & Plugins

A developer's map of the nopCommerce solution: which project owns what, how one request travels through the layers, how dependency injection wires it all together, and where your own code should live so the next upgrade doesn't eat it.

BE
nopCommerce Development Team · Bangladesh Software Solution
Annotated anatomy of the nopCommerce solution tree in Solution Explorer, with the Libraries, Presentation, Plugins and Tests folders labelled as application core, UI layer, your code and tests

You clone the source, open NopCommerce.sln, and Solution Explorer fills with folders named Libraries, Presentation, Plugins and Tests. The ticket in front of you says something small: send order data to the warehouse system, or change how a product price is calculated. The real question is not how to write that code. It is where it is allowed to live.

Understanding nopCommerce architecture answers that question once, for every ticket after it. In our deployments, almost every painful upgrade we've been asked to rescue traces back to one decision made on day one: somebody put custom logic in a core file because they couldn't see a better door.

This guide walks the solution the way we onboard new engineers at BSS: the layers and the one rule that governs them, what each project owns, how a single product-page request travels through the stack, how dependency injection connects everything, and how the plugin architecture lets you change almost anything without touching core. Every mechanism here is confirmed against nopCommerce's own developer documentation.

The Shape: An Onion, Not a Stack

Most developers picture a web app as a vertical stack: UI on top, database at the bottom. nopCommerce is better described the way its own documentation describes it, as an onion architecture. Business rules sit at the centre, and everything else wraps around them.

The one rule: dependencies point inward. A project may reference only projects that sit closer to the centre of the solution, never the other way round.

That is why Nop.Services knows nothing about controllers or Razor views, and why the whole application core can be tested and changed without touching the storefront.

In the solution, that onion is split across four top-level folders. Two of them are nopCommerce itself, one is yours, and one keeps both honest:

Libraries = application core
Nop.Core, Nop.Data and Nop.Services. Domain entities, data access and every business rule. No HTTP, no views.
Presentation = UI layer
Nop.Web.Framework and Nop.Web. The storefront, the admin area, controllers, model factories and views.
Plugins = your code
Separate projects that reference the framework and add or replace behaviour. The Tests folder sits beside them and covers the core.

Keep that picture in mind for the rest of the article. Every question about where code goes comes back to it: is this a business rule, a presentation concern, or a customisation?

The Five Core Projects and What Each Owns

Knowing the layers is the theory. Day to day, you work with five projects, and each has a sharply defined job. This is the table we pin next to a new developer's monitor:

Project Layer What it owns You read it when…
Nop.Core Core (centre) Domain entities such as Product and Order, caching abstractions, the engine, type finder and the INopStartup contract. You need an entity's shape or a shared infrastructure type.
Nop.Data Core Database access through Linq2DB, the IRepository<TEntity> interface, and FluentMigrator migrations. You add a table or need to understand how an entity is persisted.
Nop.Services Core (outer ring) Business logic, validation and calculations: products, orders, pricing, tax, shipping, plugins, events. The facade the UI talks to. You want to know why the store behaves the way it does.
Nop.Web.Framework UI (shared) Logic shared by storefront and admin: base controllers, MVC plumbing, themes, the startup classes that configure ASP.NET Core. You build a plugin — this is the project your plugin references.
Nop.Web UI (outermost) The ASP.NET Core application: public controllers, model factories, Razor views, and the Admin panel as a separate authenticated area. You trace what a page renders, or where a view comes from.

According to nopCommerce's developer documentation, the data layer has used Linq2DB as its ORM since version 4.30. It supports Microsoft SQL Server, MySQL and PostgreSQL behind the same IRepository<TEntity> API. That matters for architecture: services never write SQL for a specific database, so neither should your plugin.

Insider tip

When you can't find where a behaviour lives, search Nop.Services first, not Nop.Web. Controllers in nopCommerce are deliberately thin: they collect input, call a service or a model factory, and return a view. If a rule matters, it is almost always in a service.

Following One Request Through the Layers

The table tells you where things are. Tracing one request shows you how they cooperate. Here is what happens, in order, when a shopper opens a product page such as /apple-macbook-pro:

  • 1. Routing (Nop.Web). nopCommerce's slug routing looks up the URL record for apple-macbook-pro, finds that it belongs to a product, and hands the request to ProductController.ProductDetails.
  • 2. Controller (Nop.Web). The controller checks the product is published and visible to this customer and store, then asks a model factory for a view model. It holds no business rules of its own.
  • 3. Model factory (Nop.Web). IProductModelFactory assembles everything the view needs: price, pictures, attributes, reviews, breadcrumb. It gets every piece from services.
  • 4. Services (Nop.Services). IProductService, the price calculation service, the picture service and others apply the real rules: discounts, tier prices, tax display, ACL and store mapping.
  • 5. Cache, then data (Nop.Core + Nop.Data). Services read through IStaticCacheManager first. On a miss, IRepository<Product> runs a LINQ query that Linq2DB translates to SQL for whichever database you run.
  • 6. View (Nop.Web + theme). The model goes to the Razor view. If the active theme has its own copy of that view, the theme's version wins.

Each arrow in that path points inward. The controller knows about services; services never know about controllers. That is the onion rule working in practice, and it is also why caching behaves predictably. If you want to go deeper into that fifth step, our nopCommerce caching guide covers the per-request, in-memory and Redis layers in detail.

Controllers ask, services decide, repositories fetch. Put a business rule anywhere else and you'll end up maintaining two copies of it.

BSS Engineering

Dependency Injection: How Everything Finds Everything

Step 4 above uses half a dozen services, and the controller never creates any of them with new. That is nopCommerce dependency injection at work. Each class declares the interfaces it needs in its constructor, and the container supplies the registered implementations at runtime.

Current nopCommerce versions register services on ASP.NET Core's standard IServiceCollection. The hook for doing that is the INopStartup interface. Core uses it for its own registrations, and your plugin uses exactly the same mechanism:

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Nop.Core.Infrastructure;

namespace Nop.Plugin.Misc.OrderAudit.Infrastructure;

public class PluginNopStartup : INopStartup
{
    // Register this plugin's services with the container
    public void ConfigureServices(IServiceCollection services, IConfiguration configuration)
    {
        services.AddScoped<IOrderAuditService, OrderAuditService>();
    }

    // Add middleware to the request pipeline (none needed here)
    public void Configure(IApplicationBuilder application)
    {
    }

    // Higher values run later, so they can override earlier registrations
    public int Order => 3000;
}

Three details in that class carry most of the architecture:

  • ConfigureServices is where registrations go. Most nopCommerce services are registered as scoped, meaning one instance per HTTP request, and your services should follow that default unless you have a reason not to.
  • Configure lets a plugin add middleware to the ASP.NET Core pipeline. Most plugins leave it empty.
  • Order decides when this startup runs relative to every other one. nopCommerce's documentation is explicit that a higher value registers later, which is how you override a core registration.
Workflow diagram of how a plugin's services reach the dependency injection container: the Plugins folder is scanned, every INopStartup runs in Order, the container is built, and services are injected through constructors

Watch out

You will find older tutorials that register services in a DependencyRegistrar class with an Autofac ContainerBuilder. Those were written for an older generation of nopCommerce. On current versions, use INopStartup and IServiceCollection, as nopCommerce's own inversion-of-control documentation shows.

There is also a service locator, EngineContext.Current.Resolve<T>(), for the rare place where constructor injection isn't possible. Treat it as an escape hatch. A class that resolves its own dependencies hides them from the reader and from your tests, and a common mistake we see when auditing setups is plugins that use it everywhere.

Changing Core Behaviour Without Editing Core

With DI in place, the question from the introduction gets a real answer. Almost everything in nopCommerce is reached through an interface, so almost everything can be replaced, extended or listened to from outside core. You have five doors, and they are not interchangeable:

Mechanism Use it when Survives upgrades?
Edit core source Never, if one of the other four can do the job. Every edit becomes a merge conflict at the next upgrade. Poorly
Replace a service via DI You must change how an existing rule works, such as pricing or shipping eligibility. Inherit the core service, override the virtual method, register it with a higher Order. Yes — re-test the overridden methods
Event consumer You need to react when something happens: an order is placed, a product is updated, a customer registers. Yes
Widget in a widget zone You need to add markup to an existing page without changing its view. Yes
Theme view override You need a page to look different. Copy the view into your theme's folder at the same path and change it there. Yes — diff your views against new core views

The order of that table is also our order of preference, read bottom to top. Reach for an event or a widget first, a service replacement when you must change a rule, and core edits essentially never. For the widget route in full, see how to add a widget to a nopCommerce plugin.

Events: reacting without replacing

The warehouse ticket from the introduction is an event problem, not a service problem. You don't want to change how orders are placed; you want to know when one is. nopCommerce publishes events through IEventPublisher, and any class implementing IConsumer<T> receives them:

using Nop.Core.Domain.Orders;
using Nop.Services.Events;

namespace Nop.Plugin.Misc.OrderAudit.Services;

public class OrderPlacedConsumer : IConsumer<OrderPlacedEvent>
{
    private readonly IOrderAuditService _orderAuditService;

    public OrderPlacedConsumer(IOrderAuditService orderAuditService)
    {
        _orderAuditService = orderAuditService;
    }

    public async Task HandleEventAsync(OrderPlacedEvent eventMessage)
    {
        // Keep this fast: consumers run inside the request that raised the event
        await _orderAuditService.LogOrderPlacedAsync(eventMessage.Order);
    }
}

Notice what the consumer doesn't do: it holds no logic and touches no repository. It hands off to the plugin's own service, the one registered in PluginNopStartup above. Entity events work the same way. Implement IConsumer<EntityInsertedEvent<Product>> and you are told about every new product, whichever screen or import created it.

Plugins: The Front Door for Your Code

Everything in the last two sections ships the same way: inside a plugin. The nopCommerce plugin architecture is what turns those extension points into a deployable unit that can be installed, configured and uninstalled from the admin panel.

A plugin is an ordinary class library that references Nop.Web.Framework. Its project file sends the build output into Presentation/Nop.Web/Plugins/, where nopCommerce discovers it at startup. Each plugin carries a plugin.json descriptor:

{
  "Group": "Misc",
  "FriendlyName": "Order Audit Trail",
  "SystemName": "Misc.OrderAudit",
  "Version": "1.00",
  "SupportedVersions": [ "4.80" ],
  "Author": "Your Company",
  "DisplayOrder": 1,
  "FileName": "Nop.Plugin.Misc.OrderAudit.dll",
  "Description": "Writes an audit record whenever an order is placed."
}

SupportedVersions is the field that bites at upgrade time: nopCommerce won't load a plugin that doesn't declare the running version. The main plugin class inherits BasePlugin, and the interface it implements decides what kind of plugin it is:

Storefront and admin extensions
IWidgetPlugin renders a view component into named widget zones. IMiscPlugin covers everything that isn't a provider: integrations, background sync, admin tools like our order-audit example.
Providers core calls into
IPaymentMethod, IShippingRateComputationMethod, ITaxProvider, IExternalAuthenticationMethod and similar. Core owns the checkout flow and calls your provider at the right step.

A plugin can also bring its own layers with it: domain entities and FluentMigrator migrations for new tables, services registered through INopStartup, controllers and views for its configuration page, settings stored through ISettingService, and routes through IRouteProvider. In other words, a well-built plugin is a miniature copy of the same onion.

If you're ready to build one, our flagship guide on how to create a plugin in nopCommerce 4.80 goes from an empty project to a working entry under Local Plugins. For a provider-style plugin specifically, building a payment plugin walks through the IPaymentMethod lifecycle.

Architecture Mistakes We See in Code Audits

Most of the architecture problems we're asked to fix aren't exotic. They are the same few shortcuts, each of which works on the day it's written and costs money at the next upgrade or the next traffic spike:

Edited core files
A pricing tweak made directly in Nop.Services. It works until the upgrade, when it either silently disappears or blocks the merge. Move it into a service override inside a plugin.
Raw SQL around the repository
Direct SQL writes skip the repository, so they skip entity events and cache invalidation too. The admin shows old data and nobody knows why. Write through IRepository<T> or the owning service.
Logic in controllers and views
A discount rule inside a Razor view can't be reused by the admin, the API or an import. Business rules belong in services, where every caller gets the same answer.
Service locator everywhere
EngineContext.Current.Resolve scattered through a plugin hides its dependencies. Refactor to constructor injection so the class states what it needs.

The first mistake is the expensive one. Before a version jump, it's worth reading our guide to upgrading nopCommerce to 4.90. Stores that kept their changes in plugins tend to have a short list of work there; stores with edited core files have a long one.

Architecture review checklist

  • No custom code inside Libraries or Presentation; every change lives in a plugin or the theme.
  • Each plugin registers its services in an INopStartup class, with an Order higher than any registration it overrides.
  • Business rules sit in services; controllers and model factories only gather input and shape output.
  • All data access goes through IRepository<T>, so entity events fire and cached data is invalidated.
  • "React when X happens" logic uses IConsumer<T>, and each consumer returns quickly.
  • Every plugin.json lists the exact versions in SupportedVersions, and new tables ship as FluentMigrator migrations.
  • Theme view overrides are listed somewhere, so they can be compared with core views after an upgrade.

Frequently Asked Questions

What architecture does nopCommerce use?
nopCommerce follows an onion architecture. The application core (Nop.Core, Nop.Data and Nop.Services) sits at the centre, the UI layer (Nop.Web.Framework and Nop.Web) wraps it, and dependencies only point inward. Plugins extend the system from outside without modifying core.
How do I register a service with dependency injection in nopCommerce?
Create a class that implements INopStartup and add your registrations in its ConfigureServices method using the IServiceCollection parameter, for example by calling services.AddScoped with your interface and implementation. The same mechanism is used by nopCommerce core and by plugins.
How can a plugin override a core nopCommerce service?
Inherit from the core service, override the methods you need to change, and register your class against the same interface in an INopStartup class whose Order value is higher than the core registration. Higher Order values register later, so your implementation wins.
Should I modify nopCommerce core source code?
Almost never. Every core edit has to be re-applied or merged at each upgrade. Event consumers, widgets, service overrides through dependency injection and theme view overrides cover nearly every customisation while keeping core untouched.
Which ORM does nopCommerce use for data access?
Since version 4.30, nopCommerce uses Linq2DB, with FluentMigrator for schema migrations. Code works with tables through the generic IRepository interface, and the same code runs on Microsoft SQL Server, MySQL and PostgreSQL.

Final Thoughts

Back to that ticket. Sending order data to the warehouse is an event consumer in a plugin, handing off to a service the plugin registers through INopStartup. Changing how a price is calculated is a service override with a higher Order. Neither one touches a file in Libraries or Presentation, so neither one breaks at the next upgrade.

That is the practical value of understanding nopCommerce architecture: you stop asking "where do I put this?" ticket by ticket. The onion rule, the five projects, DI and the plugin system give the same answer every time.

If you'd rather have that structure in place from day one, or need an existing codebase untangled, here's what to look for when you hire a nopCommerce developer, or talk to the BSS team directly.