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

  1. Delete or re-parent all child categories first (leaf-to-root order)
  2. Move children to another parent, then retry the delete
  3. 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

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


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)