{"record":{"id":"d3d914e4cb621083","repo":"fullstackhero/dotnet-starter-kit","slug":"cannot-delete-a-category-that-has-child-categories-move-or","errorCode":null,"errorMessage":"Cannot delete a category that has child categories. Move or remove the children first.","messagePattern":"Cannot delete a category that has child categories\\. Move or remove the children first\\.","errorType":"http","errorClass":"CustomException","httpStatus":409,"severity":"error","filePath":"src/Modules/Catalog/Modules.Catalog/Features/v1/Categories/DeleteCategory/DeleteCategoryCommandHandler.cs","lineNumber":27,"sourceCode":"\npublic sealed class DeleteCategoryCommandHandler(CatalogDbContext dbContext)\n    : ICommandHandler<DeleteCategoryCommand, Unit>\n{\n    public async ValueTask<Unit> Handle(DeleteCategoryCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        var category = await dbContext.Categories\n            .FirstOrDefaultAsync(c => c.Id == command.CategoryId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Category {command.CategoryId} not found.\");\n\n        bool hasChildren = await dbContext.Categories\n            .AnyAsync(c => c.ParentCategoryId == category.Id, cancellationToken)\n            .ConfigureAwait(false);\n        if (hasChildren)\n        {\n            throw new CustomException(\n                \"Cannot delete a category that has child categories. Move or remove the children first.\",\n                (IEnumerable<string>?)null,\n                HttpStatusCode.Conflict);\n        }\n\n        dbContext.Categories.Remove(category);\n        await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return Unit.Value;\n    }\n}\n","sourceCodeStart":9,"sourceCodeEnd":38,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Catalog/Modules.Catalog/Features/v1/Categories/DeleteCategory/DeleteCategoryCommandHandler.cs#L9-L38","documentation":"DeleteCategoryCommandHandler refuses to delete a category that still has child categories (any category with ParentCategoryId == category.Id), throwing CustomException with HTTP 409 Conflict. This protects the hierarchy from orphaned children; the delete is not executed.","triggerScenarios":"Send/DeleteCategory command targeting a parent category that still has at least one child row in the current tenant.","commonSituations":"Bulk deletes walking a tree top-down without removing leaves first; UI allowing delete on expanded nodes; cascading cleanup scripts that ignore the hierarchy.","solutions":["Delete or re-parent all child categories first (leaf-to-root order)","Move children to another parent, then retry the delete","If deletion must succeed wholesale, orchestrate a recursive delete in one transaction"],"exampleFix":"// before\nawait mediator.Send(new DeleteCategoryCommand(parentId));\n// after\nvar children = await mediator.Send(new GetCategoriesByParentQuery(parentId));\nforeach (var child in children) await mediator.Send(new DeleteCategoryCommand(child.Id));\nawait mediator.Send(new DeleteCategoryCommand(parentId));","handlingStrategy":"validation","validationCode":"var children = await mediator.Send(new SearchCategoriesQuery(null));\nbool hasChildren = children.Items.Any(c => c.ParentCategoryId == id);\nif (hasChildren) throw new InvalidOperationException(\"Delete or move child categories first.\");","typeGuard":null,"tryCatchPattern":"try { await mediator.Send(new DeleteCategoryCommand(id)); }\ncatch (CustomException ex) when (ex.StatusCode == HttpStatusCode.Conflict) { /* prompt user to reassign children */ }","preventionTips":["Delete hierarchies leaf-first or re-parent children in the same transaction","Disable delete buttons for nodes with children in the UI","Offer an explicit 'delete subtree' flow instead of raw parent deletes"],"tags":["conflict","http-409","hierarchy","category","delete"],"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"}