nopSolutions/nopCommerce · error · ArgumentException

Admin.Catalog.Products.RelatedProducts.CyclicallyRelated ({c

Error message

Admin.Catalog.Products.RelatedProducts.CyclicallyRelated ({circularDependencyProducts})

What it means

Thrown during product XLSX import when the 'required products' relationships declared in the spreadsheet form a cycle (product A requires B which requires … back to A). nopCommerce runs isCyclicallyRequired over each required-products entry, collects any that are cyclic, and aborts with an ArgumentException naming the cyclically-related product names.

Source

Thrown at src/Libraries/Nop.Services/ExportImport/ImportManager.cs:2218

        if (_vendorSettings.MaximumProductNumber > 0 &&
            currentVendor != null)
        {
            var newProductsCount = metadata.CountProductsInFile - allProductsBySku.Count;
            if (await _productService.GetNumberOfProductsByVendorIdAsync(currentVendor.Id) + newProductsCount > _vendorSettings.MaximumProductNumber)
                throw new ArgumentException(string.Format(await _localizationService.GetResourceAsync("Admin.Catalog.Products.ExceededMaximumNumber"), _vendorSettings.MaximumProductNumber));
        }

        //validate Circular dependency for required products
        var circularDependencyProducts = new List<Product>();

        foreach (var data in metadata.RequiredProductsData)
        {
            if (isCyclicallyRequired(data, out var product))
                circularDependencyProducts.Add(product);
        }

        if (circularDependencyProducts.Any())
            throw new ArgumentException($"{await _localizationService.GetResourceAsync("Admin.Catalog.Products.RelatedProducts.CyclicallyRelated")} ({string.Join(", ", circularDependencyProducts.Select(p => p.Name).Distinct())})");

        //performance optimization, load all categories IDs for products in one SQL request
        var allProductsCategoryIds = await _categoryService.GetProductCategoryIdsAsync(allProductsBySku.Select(p => p.Id).ToArray());

        //performance optimization, load all categories in one SQL request
        Dictionary<CategoryKey, Category> allCategories;
        try
        {
            var allCategoryList = await _categoryService.GetAllCategoriesAsync(showHidden: true);

            allCategories = await allCategoryList
                .WhereAwait(async c => await _categoryService.CanVendorAddProductsAsync(c, allCategoryList))
                .ToDictionaryAsync(async (c, _) =>
                {
                    var keyName = await _categoryService.GetFormattedBreadCrumbAsync(c, allCategoryList);
                    return new CategoryKey(keyName, c, c.LimitedToStores ? (await _storeMappingService.GetStoresIdsWithAccessAsync(c)).ToList() : new List<int>());
                });
        }

View on GitHub (pinned to 64bdf2ff08)

Solutions

  1. Open the file and break the cycle: ensure the RequiredProducts references form a DAG (no product transitively requires itself).
  2. Use the product names in the error to locate the offending rows and remove the back-edge.
  3. Re-export and re-import after fixing the relationships.
  4. If you build required-products programmatically, validate acyclicity before writing the field.

Example fix

// before
RequiredProductIds: "2,3"   // where product 2 also requires product 1 -> cycle

// after
RequiredProductIds: "2,3"   // ensure product 2 and 3 do NOT list product 1 as required
Defensive patterns

Strategy: validation

Validate before calling

// Build a directed graph of required-products references and reject cycles before import
static bool HasCycle(Dictionary<string, List<string>> requires)
{
    var visited = new HashSet<string>(); var stack = new HashSet<string>();
    bool Dfs(string n)
    {
        if (stack.Contains(n)) return true;
        if (!visited.Add(n)) return false;
        stack.Add(n);
        if (requires.TryGetValue(n, out var deps) && deps.Any(Dfs)) return true;
        stack.Remove(n); return false;
    }
    return requires.Keys.Any(Dfs);
}
if (HasCycle(requiredProductsGraph))
    return BadRequest("Required products contain a cycle; break it before importing.");
await _importManager.ImportProductsFromXlsxAsync(stream);

Try / catch

try { await _importManager.ImportProductsFromXlsxAsync(stream); }
catch (ArgumentException ex) when (ex.Message.Contains("CyclicallyRelated"))
{ /* parse the product names from the message and tell the user to remove the back-edge */ }

Prevention

When it happens

Trigger: Importing products whose RequiredProducts column creates a circular dependency — e.g. SKU-A lists SKU-B as required and SKU-B lists SKU-A as required, or a longer chain that loops back. Fires after the vendor-quota check and before category loading.

Common situations: Hand-edited required-products fields that accidentally reference each other; export from a system that allowed cycles; copy-paste errors in the Required Product IDs / SKUs column; refactoring an existing catalog introduced a loop.

Related errors


AI-assisted analysis of nopSolutions/nopCommerce@64bdf2ff08 (2026-08-13). Data as JSON: /api/errors/49d32debb1b0c797. Report an issue: GitHub.