{"record":{"id":"5aec21fc4a864db2","repo":"fullstackhero/dotnet-starter-kit","slug":"ticket-command-ticketid-not-found-deleteticketcommandhandler","errorCode":null,"errorMessage":"Ticket {command.TicketId} not found.","messagePattern":"Ticket (.+?) not found\\.","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Tickets/Modules.Tickets/Features/v1/Tickets/DeleteTicket/DeleteTicketCommandHandler.cs","lineNumber":19,"sourceCode":"using FSH.Framework.Core.Exceptions;\nusing FSH.Modules.Tickets.Contracts.v1.Tickets;\nusing FSH.Modules.Tickets.Data;\nusing Mediator;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Tickets.Features.v1.Tickets.DeleteTicket;\n\npublic sealed class DeleteTicketCommandHandler(TicketsDbContext dbContext)\n    : ICommandHandler<DeleteTicketCommand, Unit>\n{\n    public async ValueTask<Unit> Handle(DeleteTicketCommand command, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(command);\n\n        var ticket = await dbContext.Tickets\n            .FirstOrDefaultAsync(t => t.Id == command.TicketId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException($\"Ticket {command.TicketId} not found.\");\n\n        // Soft delete: the audit interceptor converts the EF Delete into an IsDeleted flip.\n        // Comments are not auto-included, so they are left untouched and survive a Restore.\n        dbContext.Tickets.Remove(ticket);\n        await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return Unit.Value;\n    }\n}\n","sourceCodeStart":1,"sourceCodeEnd":28,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Tickets/Modules.Tickets/Features/v1/Tickets/DeleteTicket/DeleteTicketCommandHandler.cs#L1-L28","documentation":"DeleteTicketCommandHandler looks up the ticket by command.TicketId and throws NotFoundException (404) if absent, before performing the soft delete (audit interceptor flips IsDeleted). A missing id means nothing was deleted.","triggerScenarios":"DeleteTicketCommand with an unknown TicketId, an id already hard-removed, or a ticket invisible due to tenant filtering.","commonSituations":"Double-click Delete causing a second call after the first soft-deleted the ticket (if filtered out on re-query); cleanup scripts with stale ids; cross-tenant id collision.","solutions":["Treat a second 404 on the same id as idempotent success in the client and refresh the list.","Verify the ticket id exists before issuing the delete.","Catch NotFoundException and log rather than crash in batch cleanup jobs.","Confirm the caller's tenant matches the ticket's tenant."],"exampleFix":"// before\ndeleteTicket.mutate(id); // second click throws NotFoundException\n// after\nconst onDelete = (id: string) => deleteTicket.mutate(id, {\n  onError: (e) => { if (isNotFound(e)) queryClient.invalidateQueries(['tickets']); }\n});","handlingStrategy":"validation","validationCode":"var ticket = await dbContext.Tickets.FirstOrDefaultAsync(t => t.Id == id);\nif (ticket is null) throw new KeyNotFoundException($\"Ticket {id} not found; nothing deleted.\");","typeGuard":"static bool Deletable(Ticket? t) => t is { IsDeleted: false };","tryCatchPattern":"try { await mediator.Send(new DeleteTicketCommand { TicketId = id }); }\ncatch (NotFoundException) { /* already gone — treat as idempotent success */ }","preventionTips":["Disable the Delete button after the first click to avoid duplicate calls.","Treat repeated 404s on delete as success and re-sync the list.","Verify tenant scope before cross-service deletes.","Log soft-deleted ids in cleanup jobs rather than throwing."],"tags":["tickets","not-found","delete","soft-delete"],"backgroundTag":"entity-not-found","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"}