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
- Open the file and break the cycle: ensure the RequiredProducts references form a DAG (no product transitively requires itself).
- Use the product names in the error to locate the offending rows and remove the back-edge.
- Re-export and re-import after fixing the relationships.
- 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
- Treat RequiredProducts as a DAG; never let A require B and B require A.
- Validate acyclicity in code that writes the RequiredProducts field.
- Re-export after fixing cycles before re-importing.
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
- Admin.Catalog.Products.Import.CategoriesDontExist
- Admin.Catalog.Products.Import.ManufacturersDontExist
- Admin.Catalog.Products.Import.ProductAttributesDontExist
- The following specification attribute option ID(s) don't exi
- No worksheet found
AI-assisted analysis of nopSolutions/nopCommerce@64bdf2ff08 (2026-08-13).
Data as JSON: /api/errors/49d32debb1b0c797.
Report an issue: GitHub.