fullstackhero/dotnet-starter-kit · error · CustomException
Cannot delete a category that has child categories. Move or…
Error message
Cannot delete a category that has child categories. Move or remove the children first.
What it means
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.
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
Example fix
// before await mediator.Send(new DeleteCategoryCommand(parentId)); // after var children = await mediator.Send(new GetCategoriesByParentQuery(parentId)); foreach (var child in children) await mediator.Send(new DeleteCategoryCommand(child.Id)); await mediator.Send(new DeleteCategoryCommand(parentId));
Defensive patterns
Strategy: validation
Validate before calling
var children = await mediator.Send(new SearchCategoriesQuery(null));
bool hasChildren = children.Items.Any(c => c.ParentCategoryId == id);
if (hasChildren) throw new InvalidOperationException("Delete or move child categories first."); Try / catch
try { await mediator.Send(new DeleteCategoryCommand(id)); }
catch (CustomException ex) when (ex.StatusCode == HttpStatusCode.Conflict) { /* prompt user to reassign children */ } Prevention
- 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
When it happens
Trigger: Send/DeleteCategory command targeting a parent category that still has at least one child row in the current tenant.
Common situations: Bulk deletes walking a tree top-down without removing leaves first; UI allowing delete on expanded nodes; cascading cleanup scripts that ignore the hierarchy.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- A category with name
- A brand with name ' ' already exists.
- Another brand with name
- Category not found.
- A product with SKU ' ' already exists.
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/d3d914e4cb621083.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Catalog/Modules.Catalog/Features/v1/Categories/DeleteCategory/DeleteCategoryCommandHandler.cs:27
public sealed class DeleteCategoryCommandHandler(CatalogDbContext dbContext)
: ICommandHandler<DeleteCategoryCommand, Unit>
{
public async ValueTask<Unit> Handle(DeleteCategoryCommand command, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(command);
var category = await dbContext.Categories
.FirstOrDefaultAsync(c => c.Id == command.CategoryId, cancellationToken)
.ConfigureAwait(false)
?? throw new NotFoundException($"Category {command.CategoryId} not found.");
bool hasChildren = await dbContext.Categories
.AnyAsync(c => c.ParentCategoryId == category.Id, cancellationToken)
.ConfigureAwait(false);
if (hasChildren)
{
throw new CustomException(
"Cannot delete a category that has child categories. Move or remove the children first.",
(IEnumerable<string>?)null,
HttpStatusCode.Conflict);
}
dbContext.Categories.Remove(category);
await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return Unit.Value;
}
}
View on GitHub (pinned to 3f2959e683)