fullstackhero/dotnet-starter-kit · error · NotFoundException
file not found
Error message
file not found
What it means
DeleteFileCommandHandler queries FileAssets by cmd.FileAssetId and throws NotFoundException when the row is absent. Because the DbContext applies global query filters (tenant isolation + soft delete), 'not found' also covers rows that exist but are invisible to the current tenant/context.
Solutions
- Treat repeated 'file not found' on delete as success (idempotent delete) in the client, or pre-check existence.
- Verify the tenant context of the request matches the file's tenant.
- Check for soft-deleted rows with IgnoreQueryFilters to confirm whether the id ever existed.
- Refresh client caches after any delete so stale ids are not replayed.
Example fix
// before
await client.DeleteFileAsync(id);
// after (idempotent client handling)
try { await client.DeleteFileAsync(id); }
catch (ApiException e) when (e.StatusCode == 404) { /* already deleted — ignore */ } Defensive patterns
Strategy: try-catch
Validate before calling
var exists = await db.FileAssets.AnyAsync(x => x.Id == cmd.FileAssetId, ct); if (!exists) return; // nothing to delete — treat as idempotent success
Type guard
bool IsValidFileAssetId(Guid id) => id != Guid.Empty;
Try / catch
catch (NotFoundException) { /* idempotent delete: already gone */ return Results.NoContent(); } Prevention
- Make delete flows idempotent on the client: 404 after delete equals success.
- Debounce/double-submit guard delete buttons.
- Retry only on network errors, never blindly replay after a confirmed response.
- Confirm tenant context before assuming data loss on 404.
When it happens
Trigger: Delete endpoint/command invoked with a FileAssetId that does not exist, was already soft-deleted, lives in another tenant, or is a fabricated/garbage GUID from the client.
Common situations: Double-click delete: the second call hits the now soft-deleted row; client retries after network timeout though the first delete succeeded; deleting with a token from the wrong tenant; stale list data in the UI.
Understand the failure class
Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.
Related errors
AI-assisted analysis of fullstackhero/dotnet-starter-kit@3f2959e683 (2026-09-15).
Data as JSON: /api/errors/416ca6c74712360e.
Report an issue: GitHub.
Appendix: source
Thrown at src/Modules/Files/Modules.Files/Features/v1/DeleteFile/DeleteFileCommandHandler.cs:25
using Mediator;
using Microsoft.EntityFrameworkCore;
namespace FSH.Modules.Files.Features.v1.DeleteFile;
public sealed class DeleteFileCommandHandler(
FilesDbContext db,
FileAccessPolicyRegistry policies,
ICurrentUser currentUser)
: ICommandHandler<DeleteFileCommand, Unit>
{
public async ValueTask<Unit> Handle(DeleteFileCommand cmd, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(cmd);
var f = await db.FileAssets
.FirstOrDefaultAsync(x => x.Id == cmd.FileAssetId, cancellationToken)
.ConfigureAwait(false)
?? throw new NotFoundException("file not found");
var userId = currentUser.GetUserId().ToString();
var policy = policies.Resolve(f.OwnerType)
?? throw new ForbiddenException("no policy");
var ctx = new FileAccessContext(f.Id, f.OwnerType, f.OwnerId, f.CreatedByUserId, (int)f.Visibility);
if (!await policy.CanDeleteAsync(ctx, userId, cancellationToken).ConfigureAwait(false))
{
throw new ForbiddenException("not allowed to delete this file");
}
// Soft-delete: AuditableEntitySaveChangesInterceptor sets IsDeleted/DeletedOnUtc/DeletedBy on
// Remove() for ISoftDeletable; byte purge runs later via PurgeDeletedFilesJob post-retention.
db.FileAssets.Remove(f);
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
return Unit.Value;
}
}
View on GitHub (pinned to 3f2959e683)