{"record":{"id":"e9789eb8762db647","repo":"fullstackhero/dotnet-starter-kit","slug":"setting-this-parent-would-create-a-cycle","errorCode":null,"errorMessage":"Setting this parent would create a cycle.","messagePattern":"Setting this parent would create a cycle\\.","errorType":"exception","errorClass":"CustomException","httpStatus":400,"severity":"error","filePath":"src/Modules/Catalog/Modules.Catalog/Features/v1/Categories/UpdateCategory/UpdateCategoryCommandHandler.cs","lineNumber":39,"sourceCode":"\n        if (command.ParentCategoryId is { } parentId)\n        {\n            if (parentId == category.Id)\n            {\n                throw new CustomException(\n                    \"A category cannot be its own parent.\",\n                    (IEnumerable<string>?)null,\n                    HttpStatusCode.BadRequest);\n            }\n\n            // Walk parent chain to detect cycles (parent → ancestor of self)\n            var visited = new HashSet<Guid> { category.Id };\n            Guid? cursor = parentId;\n            while (cursor is { } cur)\n            {\n                if (!visited.Add(cur))\n                {\n                    throw new CustomException(\n                        \"Setting this parent would create a cycle.\",\n                        (IEnumerable<string>?)null,\n                        HttpStatusCode.BadRequest);\n                }\n                cursor = await dbContext.Categories\n                    .Where(c => c.Id == cur)\n                    .Select(c => c.ParentCategoryId)\n                    .FirstOrDefaultAsync(cancellationToken)\n                    .ConfigureAwait(false);\n            }\n        }\n\n        category.Update(command.Name, command.Description, command.ParentCategoryId);\n\n        bool slugTaken = await dbContext.Categories\n            .AnyAsync(c => c.Slug == category.Slug && c.Id != category.Id, cancellationToken)\n            .ConfigureAwait(false);\n        if (slugTaken)","sourceCodeStart":21,"sourceCodeEnd":57,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Catalog/Modules.Catalog/Features/v1/Categories/UpdateCategory/UpdateCategoryCommandHandler.cs#L21-L57","documentation":"UpdateCategory walks the ancestor chain starting at the proposed parent, tracking visited ids; if the walk revisits an id (or reaches the category itself) the new parent would create a cycle, so it throws CustomException with 400 BadRequest. This prevents parent loops like A→B→A in the category tree.","triggerScenarios":"PATCH update setting parent P such that P's ancestor chain already contains (or leads back to) the category being updated — e.g. moving A under its own descendant B.","commonSituations":"Reorganizing a tree by moving a whole subtree under one of its children; concurrent edits where two users swap parents of nested categories; import scripts that don't topologically order rows.","solutions":["Choose a parent that is not a descendant of the category being moved.","Move the intermediate nodes in the correct order (detach subtree first, then re-parent).","Render the tree in the UI with descendants of the edited node disabled as parent options.","If a legitimate restructure is needed, perform multiple sequential updates via a valid intermediate state."],"exampleFix":"// before: move A under its descendant B\nawait api.updateCategory({ id: a, parentCategoryId: b }); // 400 cycle\n\n// after: move B under A first (if that's the real intent), or pick a non-descendant parent\nawait api.updateCategory({ id: b, parentCategoryId: a });","handlingStrategy":"validation","validationCode":"// client-side: disallow selecting any descendant as parent\nconst descendants = collectDescendants(tree, categoryId);\nif (descendants.includes(payload.parentCategoryId)) {\n  return formError('Cannot move a category under its own descendant.');\n}","typeGuard":null,"tryCatchPattern":"try { await api.updateCategory(payload); }\ncatch (e) { if (e.status === 400 && /cycle/i.test(e.message)) formError('That parent would create a cycle'); else throw e; }","preventionTips":["Disable descendant nodes in the parent picker","Validate the full parent chain client-side before submit","Be careful with subtree moves and restructures"],"tags":["cycle","hierarchy","bad-request","catalog"],"backgroundTag":"invalid-state-transition","analyzedSha":"3f2959e683e9f83f13e55e1678c9119f63c7e8e5","analyzedAt":"2026-09-15T22:20:53.684Z","contentChangedAt":"2026-09-15T22:20:53.684Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}