Fix nopCommerce Plugin Development Errors | OpsBlu Docs

Fix nopCommerce Plugin Development Errors

Resolve nopCommerce plugin errors including ILocalizationService.AddOrUpdateLocaleResourceAsync failures, plugin migration exceptions, dependency...

Troubleshoot and fix the most common nopCommerce plugin development errors across versions 4.30 through 4.70+. This guide covers breaking API changes, dependency injection failures, database migration exceptions, and the exact error messages you will encounter during plugin development and version upgrades.

ILocalizationService Breaking Changes

AddOrUpdateLocaleResourceAsync Removed in 4.50+

The most frequently encountered breaking change when upgrading nopCommerce plugins is the removal of AddOrUpdateLocaleResourceAsync from ILocalizationService. This method was the standard way to register locale resources during plugin installation in nopCommerce 4.40 and earlier.

Error Message:

'ILocalizationService' does not contain a definition for 'AddOrUpdateLocaleResourceAsync'
and no accessible extension method 'AddOrUpdateLocaleResourceAsync' accepting a first argument
of type 'ILocalizationService' could be found

Why This Happened:

In nopCommerce 4.50, the localization subsystem was refactored. Locale resource management was separated from ILocalizationService into ILocaleResourceService. The bulk resource methods were also restructured to reduce database round-trips during plugin installation.

Migration Path: 4.40 to 4.50+

BEFORE (nopCommerce 4.40 and earlier -- broken in 4.50+):

public class MyPlugin : BasePlugin
{
    private readonly ILocalizationService _localizationService;

    public MyPlugin(ILocalizationService localizationService)
    {
        _localizationService = localizationService;
    }

    public override async Task InstallAsync()
    {
        // THIS NO LONGER WORKS IN 4.50+
        await _localizationService.AddOrUpdateLocaleResourceAsync(new Dictionary<string, string>
        {
            ["Plugins.MyPlugin.Title"] = "My Plugin",
            ["Plugins.MyPlugin.Description"] = "Plugin description",
            ["Plugins.MyPlugin.Settings.ApiKey"] = "API Key",
            ["Plugins.MyPlugin.Settings.Enabled"] = "Enable Plugin"
        });

        await base.InstallAsync();
    }

    public override async Task UninstallAsync()
    {
        // THIS ALSO BREAKS IN 4.50+
        await _localizationService.DeleteLocaleResourcesAsync("Plugins.MyPlugin");

        await base.UninstallAsync();
    }
}

AFTER (nopCommerce 4.50+):

public class MyPlugin : BasePlugin
{
    private readonly ILocaleResourceService _localeResourceService;
    private readonly ILanguageService _languageService;

    public MyPlugin(
        ILocaleResourceService localeResourceService,
        ILanguageService languageService)
    {
        _localeResourceService = localeResourceService;
        _languageService = languageService;
    }

    public override async Task InstallAsync()
    {
        // 4.50+ approach: use ILocaleResourceService
        var languages = await _languageService.GetAllLanguagesAsync(true);

        var resources = new List<(string Name, string Value)>
        {
            ("Plugins.MyPlugin.Title", "My Plugin"),
            ("Plugins.MyPlugin.Description", "Plugin description"),
            ("Plugins.MyPlugin.Settings.ApiKey", "API Key"),
            ("Plugins.MyPlugin.Settings.Enabled", "Enable Plugin")
        };

        foreach (var language in languages)
        {
            foreach (var (name, value) in resources)
            {
                var resource = await _localeResourceService
                    .GetLocaleResourceByNameAsync(name, language.Id, false);

                if (resource == null)
                {
                    await _localeResourceService.InsertLocaleResourceAsync(
                        new LocaleStringResource
                        {
                            LanguageId = language.Id,
                            ResourceName = name,
                            ResourceValue = value
                        });
                }
                else
                {
                    resource.ResourceValue = value;
                    await _localeResourceService.UpdateLocaleResourceAsync(resource);
                }
            }
        }

        await base.InstallAsync();
    }

    public override async Task UninstallAsync()
    {
        // 4.50+ approach to removing resources
        var languages = await _languageService.GetAllLanguagesAsync(true);

        foreach (var language in languages)
        {
            var resources = (await _localeResourceService
                .GetAllResourceValuesAsync(language.Id, null))
                .Where(r => r.Key.StartsWith("Plugins.MyPlugin"))
                .ToList();

            foreach (var resource in resources)
            {
                var localeResource = await _localeResourceService
                    .GetLocaleResourceByNameAsync(resource.Key, language.Id);
                if (localeResource != null)
                    await _localeResourceService.DeleteLocaleResourceAsync(localeResource);
            }
        }

        await base.UninstallAsync();
    }
}

Extension Method Approach (4.50+)

If you prefer a cleaner pattern similar to the old API, create an extension method:

public static class LocaleResourceExtensions
{
    public static async Task AddOrUpdatePluginResourcesAsync(
        this ILocaleResourceService localeResourceService,
        ILanguageService languageService,
        Dictionary<string, string> resources)
    {
        var languages = await languageService.GetAllLanguagesAsync(true);

        foreach (var language in languages)
        {
            foreach (var kvp in resources)
            {
                var existing = await localeResourceService
                    .GetLocaleResourceByNameAsync(kvp.Key, language.Id, false);

                if (existing == null)
                {
                    await localeResourceService.InsertLocaleResourceAsync(
                        new LocaleStringResource
                        {
                            LanguageId = language.Id,
                            ResourceName = kvp.Key,
                            ResourceValue = kvp.Value
                        });
                }
                else
                {
                    existing.ResourceValue = kvp.Value;
                    await localeResourceService.UpdateLocaleResourceAsync(existing);
                }
            }
        }
    }

    public static async Task DeletePluginResourcesAsync(
        this ILocaleResourceService localeResourceService,
        ILanguageService languageService,
        string prefix)
    {
        var languages = await languageService.GetAllLanguagesAsync(true);

        foreach (var language in languages)
        {
            var allResources = await localeResourceService
                .GetAllResourceValuesAsync(language.Id, null);

            foreach (var resource in allResources
                .Where(r => r.Key.StartsWith(prefix)))
            {
                var localeResource = await localeResourceService
                    .GetLocaleResourceByNameAsync(resource.Key, language.Id);
                if (localeResource != null)
                    await localeResourceService.DeleteLocaleResourceAsync(localeResource);
            }
        }
    }
}

Usage with extension methods:

public override async Task InstallAsync()
{
    await _localeResourceService.AddOrUpdatePluginResourcesAsync(
        _languageService,
        new Dictionary<string, string>
        {
            ["Plugins.MyPlugin.Title"] = "My Plugin",
            ["Plugins.MyPlugin.Settings.ApiKey"] = "API Key"
        });

    await base.InstallAsync();
}

Version-Specific Localization API Changes

nopCommerce 4.50 to 4.60:

In 4.60, GetAllResourceValuesAsync changed its signature. The loadPublicLocales parameter was removed:

// 4.50
var resources = await _localeResourceService.GetAllResourceValuesAsync(languageId, loadPublicLocales: null);

// 4.60+ -- parameter removed
var resources = await _localeResourceService.GetAllResourceValuesAsync(languageId);

nopCommerce 4.60 to 4.70:

In 4.70, the caching mechanism for locale resources changed. If you were manually interacting with the locale resource cache, you need to update:

// 4.60
await _staticCacheManager.RemoveByPrefixAsync(NopLocalizationDefaults.LocaleStringResourcesAllPrefix);

// 4.70+ -- cache key structure changed
await _staticCacheManager.RemoveByPrefixAsync(
    NopLocalizationDefaults.LocaleStringResourcesAllCacheKey.Prefix);

Plugin Lifecycle Errors

InstallAsync Failures

Error: Plugin install hangs or times out

System.OperationCanceledException: The operation was canceled.
   at Nop.Services.Plugins.PluginService.InstallPluginAsync(PluginDescriptor pluginDescriptor)

Common causes:

  1. Long-running database operations in InstallAsync without cancellation token support
  2. Circular dependency in DI container during install
  3. Missing database table that migration should have created

Fix -- add cancellation token support:

public override async Task InstallAsync()
{
    // Break large resource inserts into batches
    var resources = GetAllResources();
    var batchSize = 50;

    for (int i = 0; i < resources.Count; i += batchSize)
    {
        var batch = resources.Skip(i).Take(batchSize);
        foreach (var resource in batch)
        {
            await _localeResourceService.InsertLocaleResourceAsync(resource);
        }
    }

    await base.InstallAsync();
}

UninstallAsync Failures

Error: Cannot uninstall plugin -- foreign key constraint

Microsoft.Data.SqlClient.SqlException: The DELETE statement conflicted with the
REFERENCE constraint "FK_MyPlugin_Table_Customer". The conflict occurred in database
"nopCommerce", table "dbo.MyPlugin_Records", column 'CustomerId'.

Fix -- clean up plugin data before uninstall:

public override async Task UninstallAsync()
{
    // Delete plugin data BEFORE removing schema
    var repository = EngineContext.Current.Resolve<IRepository<MyPluginRecord>>();
    await repository.TruncateAsync();

    // Now remove locale resources
    // ... resource cleanup ...

    // Remove settings
    await _settingService.DeleteSettingAsync<MyPluginSettings>();

    await base.UninstallAsync();
}

PluginDescriptor Version Mismatch

Error: Plugin is not compatible with the current version of nopCommerce

Plugin 'MyPlugin' is not compatible with the current version of nopCommerce.
Supported versions: 4.40. Current version: 4.60.

Fix -- update plugin.json:

{
    "Group": "Widgets",
    "FriendlyName": "My Plugin",
    "SystemName": "Widgets.MyPlugin",
    "Version": "2.00",
    "SupportedVersions": [ "4.60", "4.70" ],
    "Author": "Your Name",
    "DisplayOrder": 1,
    "FileName": "Nop.Plugin.Widgets.MyPlugin.dll",
    "Description": "My plugin description",
    "LimitedToStores": [],
    "LimitedToCustomerRoles": [],
    "DependsOnSystemNames": []
}

Important: The SupportedVersions array must include the exact major.minor version of the nopCommerce instance. "4.60" will not match a 4.70 instance.

Dependency Injection Errors

Unable to Resolve Service

Error:

System.InvalidOperationException: Unable to resolve service for type
'Nop.Plugin.Widgets.MyPlugin.Services.IMyCustomService' while attempting to activate
'Nop.Plugin.Widgets.MyPlugin.Controllers.MyPluginController'.

Cause: Service not registered in the DI container.

Fix -- create or update DependencyRegistrar:

using Microsoft.Extensions.DependencyInjection;
using Nop.Core.Configuration;
using Nop.Core.Infrastructure;
using Nop.Core.Infrastructure.DependencyManagement;

namespace Nop.Plugin.Widgets.MyPlugin.Infrastructure
{
    public class DependencyRegistrar : IDependencyRegistrar
    {
        public int Order => 1;

        public void Register(
            IServiceCollection services,
            ITypeFinder typeFinder,
            AppSettings appSettings)
        {
            services.AddScoped<IMyCustomService, MyCustomService>();
            services.AddScoped<IMyPluginRepository, MyPluginRepository>();

            // If you need to register a factory
            services.AddScoped<IMyServiceFactory>(provider =>
                new MyServiceFactory(
                    provider.GetRequiredService<IStaticCacheManager>(),
                    provider.GetRequiredService<ILogger>()));
        }
    }
}

Missing INopStartup Implementation

Error: Plugin routes not registering or middleware not loading

// No explicit error, but plugin endpoints return 404
GET /MyPlugin/Configure -> 404 Not Found

Fix -- implement INopStartup:

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

namespace Nop.Plugin.Widgets.MyPlugin.Infrastructure
{
    public class NopStartup : INopStartup
    {
        public int Order => 100;

        public void ConfigureServices(
            IServiceCollection services,
            IConfiguration configuration)
        {
            // Register services here as alternative to DependencyRegistrar
            services.AddScoped<IMyCustomService, MyCustomService>();
        }

        public void Configure(IApplicationBuilder application)
        {
            // Configure middleware or routes
        }
    }
}

Common DI Registration Mistakes

Mistake 1: Registering as wrong lifetime

// WRONG: Singleton holds DbContext which is Scoped
services.AddSingleton<IMyService, MyService>();

// RIGHT: Match or use shorter lifetime than dependencies
services.AddScoped<IMyService, MyService>();

Mistake 2: Forgetting to register the interface mapping

// WRONG: Only registers concrete type
services.AddScoped<MyService>();

// RIGHT: Register interface-to-implementation mapping
services.AddScoped<IMyService, MyService>();

Mistake 3: Registering in wrong class for nopCommerce version

// nopCommerce 4.40 and earlier: use Autofac ContainerBuilder in DependencyRegistrar
public void Register(ContainerBuilder builder, ITypeFinder typeFinder, NopConfig config)
{
    builder.RegisterType<MyService>().As<IMyService>().InstancePerLifetimeScope();
}

// nopCommerce 4.50+: use IServiceCollection in DependencyRegistrar
public void Register(IServiceCollection services, ITypeFinder typeFinder, AppSettings appSettings)
{
    services.AddScoped<IMyService, MyService>();
}

The switch from Autofac to the built-in Microsoft DI container in 4.50 is a major migration point. If your DependencyRegistrar still references ContainerBuilder, it will not compile.

Database Migration Errors

FluentMigrator Exceptions

Error: Migration failed during plugin install

FluentMigrator.Runner.Exceptions.MissingMigrationsException:
No migrations found in assemblies 'Nop.Plugin.Widgets.MyPlugin'

Fix -- ensure your migration class is properly decorated:

using FluentMigrator;
using Nop.Data.Migrations;

namespace Nop.Plugin.Widgets.MyPlugin.Data
{
    [NopMigration("2024-01-15 12:00:00", "Widgets.MyPlugin base schema",
        MigrationProcessType.Installation)]
    public class SchemaMigration : AutoReversingMigration
    {
        public override void Up()
        {
            Create.TableFor<MyPluginRecord>();
        }
    }
}

Common migration pitfalls:

  1. Missing [NopMigration] attribute (nopCommerce uses this instead of FluentMigrator's [Migration])
  2. Wrong MigrationProcessType -- use Installation for plugin install, Update for upgrades
  3. Timestamp format must be parseable as DateTime

Schema Mismatch After Failed Migration

Error:

Microsoft.Data.SqlClient.SqlException: There is already an object named
'MyPlugin_Records' in the database.

This happens when a migration partially ran (created the table) but then failed on a subsequent step. The migration is not recorded as complete in MigrationVersionInfo, so it tries to run again.

Fix -- manual cleanup:

-- Check what migrations have been recorded
SELECT * FROM [MigrationVersionInfo]
WHERE [Description] LIKE '%MyPlugin%'
ORDER BY [AppliedOn] DESC;

-- If table exists but migration is not recorded, insert the record manually
INSERT INTO [MigrationVersionInfo] ([Version], [AppliedOn], [Description])
VALUES (637123456789, GETUTCDATE(), 'Widgets.MyPlugin base schema');

-- OR drop the partially-created table and re-run the migration
DROP TABLE IF EXISTS [MyPlugin_Records];

Rolling Back Failed Migrations

nopCommerce does not provide a built-in migration rollback UI. To roll back:

-- 1. Identify the failed migration
SELECT TOP 5 * FROM [MigrationVersionInfo]
ORDER BY [AppliedOn] DESC;

-- 2. Remove the migration record
DELETE FROM [MigrationVersionInfo]
WHERE [Description] = 'Widgets.MyPlugin base schema';

-- 3. Manually reverse the schema changes
DROP TABLE IF EXISTS [MyPlugin_Records];
DROP TABLE IF EXISTS [MyPlugin_Settings];

-- 4. Restart the application and retry installation

For code-based rollback, implement IReversibleMigration:

[NopMigration("2024-01-15 12:00:00", "Widgets.MyPlugin base schema",
    MigrationProcessType.Installation)]
public class SchemaMigration : Migration
{
    public override void Up()
    {
        Create.TableFor<MyPluginRecord>();
    }

    public override void Down()
    {
        Delete.Table(nameof(MyPluginRecord));
    }
}

Entity Builder Pattern (4.50+)

In nopCommerce 4.50+, the preferred approach for table creation uses NopEntityBuilder:

using FluentMigrator.Builders.Create.Table;
using Nop.Data.Mapping.Builders;

namespace Nop.Plugin.Widgets.MyPlugin.Data
{
    public class MyPluginRecordBuilder : NopEntityBuilder<MyPluginRecord>
    {
        public override void MapEntity(CreateTableExpressionBuilder table)
        {
            table
                .WithColumn(nameof(MyPluginRecord.Name))
                    .AsString(400).NotNullable()
                .WithColumn(nameof(MyPluginRecord.ConfigValue))
                    .AsString(int.MaxValue).Nullable()
                .WithColumn(nameof(MyPluginRecord.StoreId))
                    .AsInt32().NotNullable()
                    .ForeignKey<Store>(onDelete: System.Data.Rule.Cascade);
        }
    }
}

Version Upgrade Breaking Changes

Version Transition Breaking Change Migration Path
4.30 to 4.40 IPlugin.Install() / Uninstall() became async Change signatures to Task InstallAsync() / Task UninstallAsync() and add await to all calls
4.30 to 4.40 PluginDescriptor.SupportedVersions added Add SupportedVersions array to plugin.json
4.40 to 4.50 Autofac replaced with Microsoft DI Rewrite DependencyRegistrar to use IServiceCollection instead of ContainerBuilder
4.40 to 4.50 ILocalizationService.AddOrUpdateLocaleResourceAsync removed Use ILocaleResourceService with manual insert/update pattern
4.40 to 4.50 NopConfig replaced with AppSettings Update all references in DI registrars and services
4.40 to 4.50 IRepository<T>.Table property removed Use IRepository<T>.GetAllAsync() or Table via IRepository<T>.GetAllAsync(query => query)
4.50 to 4.60 GetAllResourceValuesAsync signature change Remove loadPublicLocales parameter
4.50 to 4.60 IPermissionService.InstallPermissionsAsync signature change Update IPermissionProvider implementation to match new interface
4.60 to 4.70 Cache key structure reorganized Update NopLocalizationDefaults references to use new .Prefix property
4.60 to 4.70 BaseEntity.Id setter visibility changed Use constructor or mapped property for setting entity IDs
4.60 to 4.70 IScheduleTaskService renamed methods Update GetTaskByTypeAsync to GetTaskByTypeAsync (parameter change from string to Type)
4.70+ IWorkContext.WorkingLanguageAsync property change Use await _workContext.GetWorkingLanguageAsync() method instead of property

Common Exception Messages Reference

Error Message Cause Fix
'ILocalizationService' does not contain a definition for 'AddOrUpdateLocaleResourceAsync' API removed in nopCommerce 4.50 Switch to ILocaleResourceService with manual insert/update logic
Unable to resolve service for type 'ILocalizationService' Wrong service injected or not registered Verify DI registration in DependencyRegistrar; check if you need ILocaleResourceService instead
Plugin is not compatible with the current version of nopCommerce SupportedVersions in plugin.json does not include current version Update plugin.json to include target nopCommerce version
System.InvalidOperationException: Unable to resolve service for type Service not registered in DI container Add registration in DependencyRegistrar or INopStartup
No migrations found in assemblies Missing [NopMigration] attribute on migration class Add [NopMigration] attribute with timestamp and MigrationProcessType
There is already an object named 'X' in the database Partially-failed migration left orphan tables Manually drop the table or insert migration record into MigrationVersionInfo
ContainerBuilder does not exist in the current context Plugin still uses Autofac API from pre-4.50 Rewrite DependencyRegistrar to use IServiceCollection
The DELETE statement conflicted with the REFERENCE constraint Foreign key prevents plugin data deletion during uninstall Delete dependent records before calling base.UninstallAsync()
Could not load file or assembly 'Nop.Plugin.X' Plugin DLL not found or dependency missing Verify build output copies to Plugins/ directory; check all NuGet dependencies
NullReferenceException in PluginService.PreparePluginToInstall Malformed plugin.json or missing required fields Validate JSON structure and ensure all required fields are present
System.TypeLoadException: Method 'InstallAsync' does not have an implementation Plugin interface changed between versions Rebuild plugin against the target nopCommerce version NuGet packages
MissingMethodException: Method not found 'Void Nop.Services.Localization.ILocalizationService.DeleteLocaleResources' Non-async version removed Switch to async DeleteLocaleResourceAsync on ILocaleResourceService
InvalidOperationException: Cannot resolve scoped service from root provider Singleton service depends on scoped service Change service lifetime to Scoped or use IServiceScopeFactory

Debugging Plugin Errors

Enable Detailed Errors

// appsettings.json
{
    "Hosting": {
        "UseDetailedErrors": true
    },
    "Logging": {
        "LogLevel": {
            "Default": "Debug",
            "Nop": "Trace",
            "FluentMigrator": "Debug"
        }
    }
}

Check Plugin Load Errors in Admin

Administration > System > Log

Filter by:
- Log level: Error
- Message: Search for your plugin system name
- Date range: Time of last install attempt

Verify Plugin Assembly Loading

// Temporary diagnostic code in your plugin's InstallAsync
public override async Task InstallAsync()
{
    var assembly = typeof(MyPlugin).Assembly;
    var logger = EngineContext.Current.Resolve<ILogger>();
    await logger.InformationAsync(
        $"Plugin loading: {assembly.FullName}, Location: {assembly.Location}");

    await base.InstallAsync();
}