fullstackhero/dotnet-starter-kit · warning · CustomException

Cannot a ticket in status — reopen it first.

Error message

Cannot {action} a ticket in status {Status} — reopen it first.

What it means

The Ticket aggregate guards closed/resolved tickets: ThrowIfClosedOrResolved fires a 409 CustomException whenever a mutating action (e.g. Assign) is attempted on a ticket whose Status is Closed or Resolved. Tickets are meant to be immutable once resolved/closed until explicitly Reopen()ed. This is a domain-rule conflict, not a data error.

Solutions

  1. Check the ticket status before calling Assign (fetch it or expose Status on the DTO) and skip/warn if Closed or Resolved.
  2. Call ReopenTicket (ticket.Reopen()) first when the workflow genuinely requires reassigning a resolved ticket, then Assign.
  3. Catch CustomException with 409 Conflict in the endpoint layer and surface 'reopen the ticket first' to the user.
  4. Have the UI disable the Assign action when the ticket's status is Resolved/Closed to prevent the doomed call.

Example fix

// before
await mediator.Send(new AssignTicketCommand { TicketId = id, AssigneeUserId = user });
// after
var ticket = await dbContext.Tickets.FindAsync([id]);
if (ticket is { Status: TicketStatus.Closed or TicketStatus.Resolved })
    await mediator.Send(new ReopenTicketCommand { TicketId = id });
await mediator.Send(new AssignTicketCommand { TicketId = id, AssigneeUserId = user });
Defensive patterns

Strategy: validation

Validate before calling

var ticket = await dbContext.Tickets.AsNoTracking().FirstOrDefaultAsync(t => t.Id == id);
if (ticket is { Status: TicketStatus.Closed or TicketStatus.Resolved })
    throw new InvalidOperationException("Reopen the ticket before assigning.");

Type guard

static bool CanAssign(Ticket t) => t is { Status: not TicketStatus.Closed and not TicketStatus.Resolved };

Try / catch

try { await mediator.Send(new AssignTicketCommand { TicketId = id, AssigneeUserId = user }); }
catch (CustomException ex) when (ex.StatusCode == HttpStatusCode.Conflict) { NotifyUser("Ticket must be reopened first."); }

Prevention

When it happens

Trigger: Calling ticket.Assign(assigneeUserId) (via AssignTicketCommand) while Status == Closed or Resolved; any future action routed through ThrowIfClosedOrResolved with action names like 'assign' on a closed/resolved ticket.

Common situations: Stale UI showing a resolved ticket where an operator clicks 'Assign'; two agents working the same ticket — one closes it, the other submits an assignment that lands after; automated workflows assigning resolved tickets for follow-up.

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/eecb2ec81812d83d. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Tickets/Modules.Tickets/Domain/Ticket.cs:234

    private void TransitionStatus(TicketStatus next)
    {
        if (next == Status)
        {
            return;
        }

        var previous = Status;
        Status = next;
        AddDomainEvent(DomainEvent.Create<TicketStatusChangedDomainEvent>(
            (id, ts) => new TicketStatusChangedDomainEvent(Id, previous, next, id, ts)));
    }

    private void ThrowIfClosedOrResolved(string action)
    {
        if (Status is TicketStatus.Closed or TicketStatus.Resolved)
        {
            throw new CustomException(
                $"Cannot {action} a ticket in status {Status} — reopen it first.",
                (IEnumerable<string>?)null,
                HttpStatusCode.Conflict);
        }
    }
}

View on GitHub (pinned to 3f2959e683)