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
- 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.
- Check you are pointed at the correct environment/database; ids are not portable across environments.
- If the request was deleted, create a new top-up request rather than approving the stale id.
- 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
- Always source ids from a fresh GET, never from cached/stale UI state.
- Don't reuse ids across environments (dev/test/prod).
- Handle reject/delete flows in the UI so users can't approve removed requests.
- Log the id and environment on NotFound to spot cross-environment misuse quickly.
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)