Skip to content

Developer guide

How to Build a nopCommerce Plugin for 5.0, and Update a 4.90 One

A nopCommerce 5.00 plugin is a .NET 10 class library in the Plugins folder of the source, with a plugin.json file that names it and lists the supported version, and a class that implements BasePlugin plus an interface such as IWidgetPlugin. This tutorial builds a small widget plugin with settings and an admin page, then lists what changes when you update a 4.90 plugin to 5.00.

By CartenvoPublished Updated

Facts checked against the nopCommerce 5.00.0 source code on .

Target
nopCommerce 5.00
Framework
net10.0
Example
Widget plugin
On this page

Most nopCommerce plugin tutorials were written for 4.x and still show .NET 8 or 9 and AutoMapper. This one targets nopCommerce 5.00, using the same patterns as the plugins that ship with it. The porting table at the end comes from moving our own StorePulse plugin to 5.00.

Before you start

  • The nopCommerce 5.00 source from GitHub, and the .NET 10 SDK. The source’s global.json asks for SDK 10.0.401 or a later feature release.
  • Visual Studio 2026, JetBrains Rider or VS Code with the C# extension.
  • A local database and a working install of the source, so you can run the store and debug the plugin.

The example plugin, Hello bar, shows a message from the admin area at the top of every page. These are its files:

Plugin folder

Plugins/
  Nop.Plugin.Widgets.HelloBar/
    Nop.Plugin.Widgets.HelloBar.csproj
    plugin.json
    logo.png
    HelloBarSettings.cs
    HelloBarPlugin.cs
    Components/HelloBarViewComponent.cs
    Controllers/WidgetsHelloBarController.cs
    Models/ConfigurationModel.cs
    Views/_ViewImports.cshtml
    Views/Configure.cshtml
    Views/PublicInfo.cshtml

1. Create the project

Add a class library called Nop.Plugin.Widgets.HelloBar under src/Plugins and replace its project file. The output goes straight into Nop.Web/Plugins, and the NopClear target removes copies of nopCommerce’s own libraries, which would otherwise clash at load time.

Nop.Plugin.Widgets.HelloBar.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <OutputPath>$(SolutionDir)\Presentation\Nop.Web\Plugins\Widgets.HelloBar</OutputPath>
    <OutDir>$(OutputPath)</OutDir>
    <CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

  <ItemGroup>
    <Content Include="plugin.json;logo.png;Views\**\*.cshtml">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </Content>
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="$(SolutionDir)\Presentation\Nop.Web\Nop.Web.csproj" />
    <ClearPluginAssemblies Include="$(SolutionDir)\Build\ClearPluginAssemblies.proj" />
  </ItemGroup>

  <!-- Removes nopCommerce's own DLLs from the plugin's output folder -->
  <Target Name="NopTarget" AfterTargets="Build">
    <MSBuild Projects="@(ClearPluginAssemblies)" Properties="PluginPath=$(OutDir)" Targets="NopClear" />
  </Target>
</Project>

2. Describe it in plugin.json

plugin.json

{
  "Group": "Widgets",
  "FriendlyName": "Hello bar",
  "SystemName": "Widgets.HelloBar",
  "Version": "5.00.1",
  "SupportedVersions": [ "5.00" ],
  "Author": "Your company",
  "DisplayOrder": 1,
  "FileName": "Nop.Plugin.Widgets.HelloBar.dll",
  "Description": "Shows a message bar at the top of every page"
}

SystemName must be unique and is used in settings and URLs. SupportedVersions decides whether nopCommerce loads the plugin at all; FileName must match the built DLL.

3. Add a settings class

HelloBarSettings.cs

using Nop.Core.Configuration;

namespace Nop.Plugin.Widgets.HelloBar;

public class HelloBarSettings : ISettings
{
    public string Message { get; set; }
}

Any class implementing ISettings is stored in the Setting table and can be injected anywhere. Settings can differ per store in a multi-store installation.

4. Write the plugin class

The plugin class tells nopCommerce what kind of plugin this is, where it shows, and what to do on install and uninstall. A widget plugin implements IWidgetPlugin:

HelloBarPlugin.cs

using Nop.Core.Domain.Cms;
using Nop.Plugin.Widgets.HelloBar.Components;
using Nop.Services.Cms;
using Nop.Services.Configuration;
using Nop.Services.Helpers;      // IWebHelper lives here in 5.00
using Nop.Services.Localization;
using Nop.Services.Plugins;
using Nop.Web.Framework.Infrastructure;

namespace Nop.Plugin.Widgets.HelloBar;

public class HelloBarPlugin : BasePlugin, IWidgetPlugin
{
    protected readonly ILocalizationService _localizationService;
    protected readonly ISettingService _settingService;
    protected readonly IWebHelper _webHelper;
    protected readonly WidgetSettings _widgetSettings;

    public HelloBarPlugin(ILocalizationService localizationService, ISettingService settingService,
        IWebHelper webHelper, WidgetSettings widgetSettings)
    {
        _localizationService = localizationService;
        _settingService = settingService;
        _webHelper = webHelper;
        _widgetSettings = widgetSettings;
    }

    public bool HideInWidgetList => false;

    public Task<IList<string>> GetWidgetZonesAsync() =>
        Task.FromResult<IList<string>>([PublicWidgetZones.HeaderBefore]);

    public Type GetWidgetViewComponent(string widgetZone) => typeof(HelloBarViewComponent);

    public override string GetConfigurationPageUrl() =>
        $"{_webHelper.GetStoreLocation()}Admin/WidgetsHelloBar/Configure";

    public override async Task InstallAsync()
    {
        await _settingService.SaveSettingAsync(new HelloBarSettings { Message = "Free delivery over $50" });

        if (!_widgetSettings.ActiveWidgetSystemNames.Contains("Widgets.HelloBar"))
        {
            _widgetSettings.ActiveWidgetSystemNames.Add("Widgets.HelloBar");
            await _settingService.SaveSettingAsync(_widgetSettings);
        }

        await _localizationService.AddOrUpdateLocaleResourceAsync(new Dictionary<string, string>
        {
            ["Plugins.Widgets.HelloBar.Message"] = "Message",
            ["Plugins.Widgets.HelloBar.Message.Hint"] = "The text shown at the top of every page."
        });

        await base.InstallAsync();
    }

    public override async Task UninstallAsync()
    {
        _widgetSettings.ActiveWidgetSystemNames.Remove("Widgets.HelloBar");
        await _settingService.SaveSettingAsync(_widgetSettings);
        await _settingService.DeleteSettingAsync<HelloBarSettings>();
        await _localizationService.DeleteLocaleResourcesAsync("Plugins.Widgets.HelloBar");

        await base.UninstallAsync();
    }
}

Install adds the widget to the active list, which saves a trip to Configuration → Widgets. Uninstall removes everything install added. Other plugin types implement other interfaces, such as IPaymentMethod, IShippingRateComputationMethod, ITaxProvider or IMiscPlugin.

5. Render the widget

A view component renders the widget for each zone it is registered in:

Components/HelloBarViewComponent.cs

using Microsoft.AspNetCore.Mvc;
using Nop.Web.Framework.Components;

namespace Nop.Plugin.Widgets.HelloBar.Components;

public class HelloBarViewComponent : NopViewComponent
{
    protected readonly HelloBarSettings _settings;

    public HelloBarViewComponent(HelloBarSettings settings)
    {
        _settings = settings;
    }

    public IViewComponentResult Invoke(string widgetZone, object additionalData)
    {
        if (string.IsNullOrWhiteSpace(_settings.Message))
            return Content(string.Empty);

        return View("~/Plugins/Widgets.HelloBar/Views/PublicInfo.cshtml", _settings.Message);
    }
}

Views/PublicInfo.cshtml

@model string

<div class="hello-bar" role="note">@Model</div>

Copy Views/_ViewImports.cshtml from a bundled plugin such as Widgets.GoogleAnalytics so the nopCommerce tag helpers are available in your views. In 5.00 a theme can override PublicInfo.cshtml without touching the plugin; see overriding plugin views.

6. Add the configuration page

The admin page is a normal MVC controller in the Admin area. The attributes make it admin-only, protect the form against cross-site requests and check the permission to manage widgets:

Controllers/WidgetsHelloBarController.cs

using Microsoft.AspNetCore.Mvc;
using Nop.Plugin.Widgets.HelloBar.Models;
using Nop.Services.Configuration;
using Nop.Services.Messages;
using Nop.Services.Security;
using Nop.Web.Framework;
using Nop.Web.Framework.Controllers;
using Nop.Web.Framework.Mvc.Filters;

namespace Nop.Plugin.Widgets.HelloBar.Controllers;

[Area(AreaNames.ADMIN)]
[AuthorizeAdmin]
[AutoValidateAntiforgeryToken]
public class WidgetsHelloBarController : BasePluginController
{
    protected readonly INotificationService _notificationService;
    protected readonly ISettingService _settingService;

    public WidgetsHelloBarController(INotificationService notificationService, ISettingService settingService)
    {
        _notificationService = notificationService;
        _settingService = settingService;
    }

    [CheckPermission(StandardPermission.Configuration.MANAGE_WIDGETS)]
    public async Task<IActionResult> Configure()
    {
        var settings = await _settingService.LoadSettingAsync<HelloBarSettings>();
        return View("~/Plugins/Widgets.HelloBar/Views/Configure.cshtml",
            new ConfigurationModel { Message = settings.Message });
    }

    [HttpPost]
    [CheckPermission(StandardPermission.Configuration.MANAGE_WIDGETS)]
    public async Task<IActionResult> Configure(ConfigurationModel model)
    {
        var settings = await _settingService.LoadSettingAsync<HelloBarSettings>();
        settings.Message = model.Message;
        await _settingService.SaveSettingAsync(settings);

        _notificationService.SuccessNotification("Settings saved.");
        return await Configure();
    }
}

Models/ConfigurationModel.cs

using Nop.Web.Framework.Models;
using Nop.Web.Framework.Mvc.ModelBinding;

namespace Nop.Plugin.Widgets.HelloBar.Models;

public record ConfigurationModel : BaseNopModel
{
    [NopResourceDisplayName("Plugins.Widgets.HelloBar.Message")]
    public string Message { get; set; }
}

Views/Configure.cshtml

@model Nop.Plugin.Widgets.HelloBar.Models.ConfigurationModel
@{
    Layout = "_ConfigurePlugin";
}

<form asp-controller="WidgetsHelloBar" asp-action="Configure" method="post">
    <div class="cards-group">
        <div class="card card-default">
            <div class="card-body">
                <div class="form-group row">
                    <div class="col-md-3"><nop-label asp-for="Message" /></div>
                    <div class="col-md-9"><nop-editor asp-for="Message" /></div>
                </div>
                <button type="submit" class="btn btn-primary">@T("Admin.Common.Save")</button>
            </div>
        </div>
    </div>
</form>

The _ConfigurePlugin layout gives the page the standard plugin header, and nop-label and nop-editor render the field with its hint, the same as the rest of the admin area. The URL matches the default admin route, so this plugin needs no route provider.

7. Build and install

  1. Build the solution. The plugin appears in Presentation/Nop.Web/Plugins/Widgets.HelloBar with its DLL, plugin.json and views.
  2. Run the store and go to Configuration → Local plugins. Hello bar is listed under Widgets.
  3. Click Install, restart when prompted, then Configure to change the message.
  4. To ship it, zip the output folder. Upload it on another store with “Upload plugin or theme”.

Adding database tables

A plugin that stores records defines an entity deriving from BaseEntity and a migration that runs on install. In 5.00 the migration helpers are extension methods on the migration itself:

Data/SchemaMigration.cs

using FluentMigrator;
using Nop.Data.Extensions;
using Nop.Data.Migrations;
using Nop.Plugin.Widgets.HelloBar.Domain;

namespace Nop.Plugin.Widgets.HelloBar.Data;

[NopMigration("2026-10-28 09:00:00", "Widgets.HelloBar schema", MigrationProcessType.Installation)]
public class SchemaMigration : Migration
{
    public override void Up() => this.CreateTableIfNotExists<HelloBarMessage>();

    public override void Down() => this.DeleteTableIfExists<HelloBarMessage>();
}

Use IRepository<HelloBarMessage> to read and write, so caching and events work. Migrations for later versions of the plugin run when the plugin’s version changes, which is why every release should raise Version in plugin.json.

Updating a 4.90 plugin to nopCommerce 5.00

Change the target framework and the supported version first, then build: the compiler finds most of the rest. These are the changes we had to make, and their 5.00 replacements:

API changes when moving a plugin from nopCommerce 4.90 to 5.00
Area4.905.00
Target frameworknet9.0net10.0, with the .NET 10 SDK and SupportedVersions "5.00" in plugin.json
Object mappingAutoMapper: class MapperConfiguration : Profile, IOrderedMapperProfileMapster underneath: derive from BaseMapperProfile (Nop.Core.Infrastructure.Mapper). CreateMap and ForMember calls carry over; AutoMapper isn’t referenced any more
IWebHelperNop.CoreNop.Services.Helpers
Async LINQ on repositoriesLinqToDB extension methodslinq2db 6 moved them: add using LinqToDB.Async where you call them
Settings and resources in migrationsSynchronous LoadSetting, SaveSetting and AddOrUpdateLocaleResourceRemoved. Use the migration extensions in Nop.Web.Framework.Extensions, such as this.SetSettingIfNotExists and this.AddOrUpdateLocaleResource, or the async service methods elsewhere
Creating tablesCreate.TableFor<T>()this.CreateTableIfNotExists<T>()
RemotePost (redirect payment forms)Post() and the Method propertyPostAsync()
Attribute picturesProductAttributeValue.PictureId and ProductAttributeCombination.PictureIdRemoved; use the ProductAttributeValuePicture and ProductAttributeCombinationPicture tables
Forums, news and pollsTypes in Nop.Core.Domain.Forums, .News, .Polls and their servicesMoved into the Misc.Forums, Misc.News and Misc.Polls plugins under new namespaces

Store owners can find which installed plugins use these APIs before upgrading: the free StorePulse plugin reads each plugin DLL’s references and lists the ones that need an update. For the release as a whole, read what changes in nopCommerce 5.0.

Frequently asked questions

Do I need the nopCommerce source code to build a plugin?

It is the easiest way. Put the plugin project in the Plugins folder of the source solution and reference Nop.Web, as the bundled plugins do; building it puts the output straight into Nop.Web/Plugins. Building against the no-source package is possible but harder to debug.

Can one plugin support nopCommerce 4.90 and 5.00?

Not as one DLL: 4.90 runs on .NET 9 and 5.00 on .NET 10, with different APIs. You can keep one source and build it twice, with conditional compilation for the differences, and ship both builds in one marketplace package. That is how we ship StorePulse for 4.80, 4.90 and 5.00.

Why doesn’t my plugin appear in Local plugins?

Check that plugin.json lists the running version in SupportedVersions, that FileName matches the DLL name, and that the plugin.json and views are copied to the output folder. Then click “Reload list of plugins” and restart the application.

Where should plugin settings go?

In a class that implements ISettings. nopCommerce stores each property in the Setting table, per store if needed, and you inject the class into controllers, services and view components. Use your own table only for lists of records, not for configuration.

Do I have to change core files?

Almost never. Widgets, events, overridden services in the dependency container, route providers and, in 5.00, theme overrides of plugin views cover most needs. Keeping changes in plugins is what keeps a store upgradeable.

Next steps

Start from a bundled plugin of the same type as yours, keep everything in the plugin, and raise the version with every release. If you need a plugin built or a set of 4.x plugins moved to 5.00, send us the list and we will estimate the work.

Related

Related guides

Moving plugins to nopCommerce 5.0?

Send us your plugin list or the plugin you need, and we will estimate the port or the build.