Blog

Dependency Injection in nopCommerce Plugins

Dependency injection is easier to understand from real code than from a textbook definition. nopCommerce is a good place to see it clearly, because writing a plugin is mostly an exercise in DI. You declare what your code needs, and the framework hands it to you.

This post is deliberately narrow. It is not a general plugin tutorial. It is the set of dependency injection scenarios you actually run into when you extend nopCommerce, and how each one works. The concepts transfer to any ASP.NET Core app, so what you learn here is really just ASP.NET Core DI seen through a real codebase.

The one-line version of DI

Dependency injection means a class does not create the things it depends on, it receives them. Something else, the container, builds those dependencies and passes them in, usually through the constructor. That inversion is what makes code loosely coupled and testable.

Worth knowing for context: modern nopCommerce (the 4.x line since 4.30) uses the built-in ASP.NET Core container, Microsoft.Extensions.DependencyInjection. Earlier versions used Autofac. So the DI skills you build here are just ASP.NET Core DI skills, not something nopCommerce-specific.

How nopCommerce wires your plugin's services

The mechanism is worth being able to describe end to end. nopCommerce finds your registration code by scanning assemblies, runs it to populate the container, and then resolves your services on demand.

A vertical flow of how nopCommerce wires a plugin's services. First the application starts and begins configuring services. Then ITypeFinder scans every assembly, including your plugin, for IDependencyRegistrar and INopStartup. The registrars run in ascending Order, each adding service mappings, and a higher Order runs later and can override. Those mappings go into the IServiceCollection container as interface-to-implementation pairs, each with a lifetime. Finally, at request time, the container builds your service and passes its dependencies into the constructor.

Concretely, you register services in one of two places, both discovered automatically by ITypeFinder:

  • IDependencyRegistrar. Its method is Register(IServiceCollection services, ITypeFinder typeFinder, AppSettings appSettings), and it exposes an Order property. This is the classic plugin registration hook.
  • INopStartup. Its ConfigureServices(IServiceCollection services, IConfiguration configuration) runs at startup too, and it also lets you wire up middleware in Configure. This is where I register plugin services that need configuration.

The Order property matters more than it looks, which I will come back to under overriding.

Service lifetimes

The lifetime you register a service with decides how long an instance lives and how often it is reused. It is also where a class of subtle bugs comes from, so it is worth being precise.

A diagram of service lifetimes as nested scopes. The outer box is the application, where a Singleton lives as one instance for the whole app, used for shared, stateless, costly-to-build things like caches and config. Inside it is the HTTP request scope, where a Scoped service lives as one per web request, which is where nopCommerce services live by default. Inside that are Transient instances, a new instance on every injection, for lightweight stateless helpers. A warning notes the captive dependency problem: never inject a Scoped service into a Singleton, because the Singleton would hold it for the app's whole life.

  • Transient: a new instance every time it is injected.
  • Scoped: one instance per web request. This is the nopCommerce default and where most services and repositories live.
  • Singleton: one instance for the whole application.

The trap to watch for is the captive dependency: if you inject a Scoped service into a Singleton, the Singleton captures it and holds that one instance for the app's entire life, which breaks the per-request assumption. Because nopCommerce services are Scoped, this is a real risk whenever you write a Singleton.

The scenarios you actually hit

Here are the DI situations that come up over and over in a real plugin. Each one is the container doing the wiring so your code just declares what it needs.

Six common dependency injection scenarios in a nopCommerce plugin, each with a code hint. Register your service, mapping an interface to your implementation with services.AddScoped of IMyService and MyService. Inject framework services by taking nop services through the constructor, such as IWorkContext and ISettingService. Override a core service by re-registering with a higher Order so the last registration wins. Access your data by injecting the generic repository IRepository of your entity. React to domain events by implementing IConsumer of EntityInsertedEvent of Product, which nop discovers and wires. Run background work with a schedule task class implementing IScheduleTask, resolved fresh on each run.

Walking through them:

  1. Register your own service. In your registrar, map the interface to the implementation with a lifetime: services.AddScoped<IMyService, MyService>();. Scoped is the usual choice, matching the rest of nopCommerce.

  2. Inject framework services. Your service or controller takes nopCommerce services in its constructor and the container supplies them. Common ones are IWorkContext, IStoreContext, ISettingService, ILocalizationService, IRepository<T>, ILogger, IWebHelper, and IEventPublisher. You never new these up, you ask for them.

  3. Override a core service. This is the one that matters most in real extension work. Because the container resolves a single service by its last registration, you can replace a built-in service with your own by registering your implementation with a higher Order, so your registrar runs after the framework's and wins. That is how you swap, say, a pricing or shipping service for custom behavior without touching core code.

  4. Access your data. Inject IRepository<TEntity> for your plugin's tables instead of talking to the DbContext directly. The repository is scoped and comes straight from the container.

  5. React to domain events. Implement IConsumer<EntityInsertedEvent<Product>> (or updated, deleted, and so on). nopCommerce discovers consumers through the same type scanning and resolves them from the container, dependencies and all, when the event fires. It is a clean extension point that is pure DI under the hood.

  6. Run background work. A IScheduleTask is resolved fresh each time it runs, so it can safely take scoped services in its constructor. This is how a plugin does recurring jobs without wiring up its own background service.

Two gotchas worth mentioning

Two things trip people up, and getting them right is what separates understanding the container from copying the syntax.

First, do not resolve services inside the Register method. At registration time the container is being built, not finished, so trying to resolve something like ISettingService there does not work cleanly. Register the mapping, and inject the service where it is used instead.

Second, prefer constructor injection over the service locator. Injecting IServiceProvider and pulling services out of it by hand hides a class's real dependencies and makes it harder to test. Constructor injection keeps them honest and visible. The exception is genuinely dynamic resolution, which is rare.

Bottom line

Extending nopCommerce is a good way to get comfortable with dependency injection, because plugins live and die by it: registration, lifetimes, overriding services, events, and tasks are all just the container doing its job. Learn it here and it is the same skill in any .NET application.

If you run a nopCommerce store and need plugin work or a custom extension built the right way, that is what I do. You can see examples of my nopCommerce work, or get a quote.

All posts