{"record":{"id":"49d32debb1b0c797","repo":"nopSolutions/nopCommerce","slug":"admin-catalog-products-relatedproducts-cyclicallyr","errorCode":null,"errorMessage":"Admin.Catalog.Products.RelatedProducts.CyclicallyRelated ({circularDependencyProducts})","messagePattern":"Admin\\.Catalog\\.Products\\.RelatedProducts\\.CyclicallyRelated \\((.+?)\\)","errorType":"validation","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/Libraries/Nop.Services/ExportImport/ImportManager.cs","lineNumber":2218,"sourceCode":"        if (_vendorSettings.MaximumProductNumber > 0 &&\r\n            currentVendor != null)\r\n        {\r\n            var newProductsCount = metadata.CountProductsInFile - allProductsBySku.Count;\r\n            if (await _productService.GetNumberOfProductsByVendorIdAsync(currentVendor.Id) + newProductsCount > _vendorSettings.MaximumProductNumber)\r\n                throw new ArgumentException(string.Format(await _localizationService.GetResourceAsync(\"Admin.Catalog.Products.ExceededMaximumNumber\"), _vendorSettings.MaximumProductNumber));\r\n        }\r\n\r\n        //validate Circular dependency for required products\r\n        var circularDependencyProducts = new List<Product>();\r\n\r\n        foreach (var data in metadata.RequiredProductsData)\r\n        {\r\n            if (isCyclicallyRequired(data, out var product))\r\n                circularDependencyProducts.Add(product);\r\n        }\r\n\r\n        if (circularDependencyProducts.Any())\r\n            throw new ArgumentException($\"{await _localizationService.GetResourceAsync(\"Admin.Catalog.Products.RelatedProducts.CyclicallyRelated\")} ({string.Join(\", \", circularDependencyProducts.Select(p => p.Name).Distinct())})\");\r\n\r\n        //performance optimization, load all categories IDs for products in one SQL request\r\n        var allProductsCategoryIds = await _categoryService.GetProductCategoryIdsAsync(allProductsBySku.Select(p => p.Id).ToArray());\r\n\r\n        //performance optimization, load all categories in one SQL request\r\n        Dictionary<CategoryKey, Category> allCategories;\r\n        try\r\n        {\r\n            var allCategoryList = await _categoryService.GetAllCategoriesAsync(showHidden: true);\r\n\r\n            allCategories = await allCategoryList\r\n                .WhereAwait(async c => await _categoryService.CanVendorAddProductsAsync(c, allCategoryList))\r\n                .ToDictionaryAsync(async (c, _) =>\r\n                {\r\n                    var keyName = await _categoryService.GetFormattedBreadCrumbAsync(c, allCategoryList);\r\n                    return new CategoryKey(keyName, c, c.LimitedToStores ? (await _storeMappingService.GetStoresIdsWithAccessAsync(c)).ToList() : new List<int>());\r\n                });\r\n        }\r","sourceCodeStart":2200,"sourceCodeEnd":2236,"githubUrl":"https://github.com/nopSolutions/nopCommerce/blob/64bdf2ff08c8b39e65717bcf974fb43dc2ef68f2/src/Libraries/Nop.Services/ExportImport/ImportManager.cs#L2200-L2236","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nRequiredProductIds: \"2,3\"   // where product 2 also requires product 1 -> cycle\n\n// after\nRequiredProductIds: \"2,3\"   // ensure product 2 and 3 do NOT list product 1 as required","handlingStrategy":"validation","validationCode":"// Build a directed graph of required-products references and reject cycles before import\nstatic bool HasCycle(Dictionary<string, List<string>> requires)\n{\n    var visited = new HashSet<string>(); var stack = new HashSet<string>();\n    bool Dfs(string n)\n    {\n        if (stack.Contains(n)) return true;\n        if (!visited.Add(n)) return false;\n        stack.Add(n);\n        if (requires.TryGetValue(n, out var deps) && deps.Any(Dfs)) return true;\n        stack.Remove(n); return false;\n    }\n    return requires.Keys.Any(Dfs);\n}\nif (HasCycle(requiredProductsGraph))\n    return BadRequest(\"Required products contain a cycle; break it before importing.\");\nawait _importManager.ImportProductsFromXlsxAsync(stream);","typeGuard":null,"tryCatchPattern":"try { await _importManager.ImportProductsFromXlsxAsync(stream); }\ncatch (ArgumentException ex) when (ex.Message.Contains(\"CyclicallyRelated\"))\n{ /* parse the product names from the message and tell the user to remove the back-edge */ }","preventionTips":["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."],"tags":["import","catalog","required-products","cycle","validation","nopcommerce"],"backgroundTag":null,"analyzedSha":"64bdf2ff08c8b39e65717bcf974fb43dc2ef68f2","analyzedAt":"2026-08-13T21:19:38.062Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}