fullstackhero/dotnet-starter-kit · error · NotFoundException

Top-up request not found.

Error message

Top-up request {command.Id} not found.

What it means

After resolving the caller tenant, ApproveTopupRequestCommandHandler loads the TopupRequest by Id from the (non-tenant-filtered) BillingDbContext and throws NotFoundException('Top-up request {id} not found.') when no row matches. Because BillingDbContext lacks a global tenant query filter, this NotFoundException is authoritative: the id genuinely does not exist, not merely hidden by a tenant filter.

Solutions

  1. Verify the top-up request id exists: query GET /api/v1/wallets/topup-requests (root for cross-tenant) and use an id from the response.
  2. Check you are pointed at the correct environment/database; ids are not portable across environments.
  3. If the request was deleted, create a new top-up request rather than approving the stale id.
  4. Confirm the client is not sending Guid.Empty due to a serialization bug — validate the id is a non-empty Guid before calling.

Example fix

// before
var id = Guid.Empty;
await mediator.Send(new ApproveTopupRequestCommand(id));
// after
var id = /* fetch from GET /topup-requests */ loaded.Id;
if (id == Guid.Empty) throw new InvalidOperationException("Top-up request id missing");
await mediator.Send(new ApproveTopupRequestCommand(id));
Defensive patterns

Strategy: validation

Validate before calling

if (id == Guid.Empty) throw new ArgumentException("Top-up request id required");
var known = await api.GetTopupRequestsAsync(); // ensure id comes from server data, not stale state
if (known.Items.All(r => r.Id != id)) throw new InvalidOperationException($"Unknown top-up request {id}");

Type guard

bool IsValidTopupRequestId(Guid id) => id != Guid.Empty;

Try / catch

try { await api.ApproveTopupRequestAsync(id); }
catch (NotFoundException ex) when (ex.Message.Contains("Top-up request"))
{
    // refresh the list; the request no longer exists in this environment
}

Prevention

When it happens

Trigger: Approving with a GUID that was never created, an id from a different database/environment (dev vs prod), or an id of a top-up request that was deleted; also passing a malformed/empty Guid that maps to Guid.Empty.

Common situations: Stale links in a UI after reseeding the database; copying an id from logs of another environment; race where the request was rejected/deleted between listing and approving; typo'd id in a test payload.

Understand the failure class

Background: Record Not Found Errors: "not found", RecordNotFound, and "was not found" — what they mean and how to fix them — this error's family across 28 libraries.

Related errors


AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15). Data as JSON: /api/errors/59ca45deca923043. Report an issue: GitHub.

Appendix: source

Thrown at src/Modules/Billing/Modules.Billing/Features/v1/Wallets/ApproveTopupRequest/ApproveTopupRequestCommandHandler.cs:29

public sealed class ApproveTopupRequestCommandHandler(
    BillingDbContext db,
    IBillingService billing,
    IMultiTenantContextAccessor<AppTenantInfo> tenantAccessor)
    : ICommandHandler<ApproveTopupRequestCommand, Guid>
{
    public async ValueTask<Guid> Handle(ApproveTopupRequestCommand command, CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(command);

        var callerTenantId = tenantAccessor.MultiTenantContext?.TenantInfo?.Id
            ?? throw new UnauthorizedException("Tenant context is required.");
        var isRoot = callerTenantId == MultitenancyConstants.Root.Id;

        var request = await db.TopupRequests
            .FirstOrDefaultAsync(r => r.Id == command.Id, cancellationToken)
            .ConfigureAwait(false)
            ?? throw new NotFoundException($"Top-up request {command.Id} not found.");

        if (!isRoot && request.TenantId != callerTenantId)
        {
            throw new UnauthorizedException("You can only approve top-up requests for your own tenant.");
        }

        // For root, operate on the request's own tenant; for non-root, callerTenantId equals request.TenantId.
        var invoice = await billing.CreateTopupInvoiceAsync(request.TenantId, command.Id, cancellationToken)
            .ConfigureAwait(false);

        return invoice.Id;
    }
}

View on GitHub (pinned to 3f2959e683)