{"record":{"id":"416ca6c74712360e","repo":"fullstackhero/dotnet-starter-kit","slug":"file-not-found-deletefilecommandhandler","errorCode":null,"errorMessage":"file not found","messagePattern":"file not found","errorType":"exception","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"src/Modules/Files/Modules.Files/Features/v1/DeleteFile/DeleteFileCommandHandler.cs","lineNumber":25,"sourceCode":"using Mediator;\nusing Microsoft.EntityFrameworkCore;\n\nnamespace FSH.Modules.Files.Features.v1.DeleteFile;\n\npublic sealed class DeleteFileCommandHandler(\n    FilesDbContext db,\n    FileAccessPolicyRegistry policies,\n    ICurrentUser currentUser)\n    : ICommandHandler<DeleteFileCommand, Unit>\n{\n    public async ValueTask<Unit> Handle(DeleteFileCommand cmd, CancellationToken cancellationToken)\n    {\n        ArgumentNullException.ThrowIfNull(cmd);\n\n        var f = await db.FileAssets\n            .FirstOrDefaultAsync(x => x.Id == cmd.FileAssetId, cancellationToken)\n            .ConfigureAwait(false)\n            ?? throw new NotFoundException(\"file not found\");\n\n        var userId = currentUser.GetUserId().ToString();\n        var policy = policies.Resolve(f.OwnerType)\n            ?? throw new ForbiddenException(\"no policy\");\n        var ctx = new FileAccessContext(f.Id, f.OwnerType, f.OwnerId, f.CreatedByUserId, (int)f.Visibility);\n        if (!await policy.CanDeleteAsync(ctx, userId, cancellationToken).ConfigureAwait(false))\n        {\n            throw new ForbiddenException(\"not allowed to delete this file\");\n        }\n\n        // Soft-delete: AuditableEntitySaveChangesInterceptor sets IsDeleted/DeletedOnUtc/DeletedBy on\n        // Remove() for ISoftDeletable; byte purge runs later via PurgeDeletedFilesJob post-retention.\n        db.FileAssets.Remove(f);\n        await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);\n        return Unit.Value;\n    }\n}\n","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/fullstackhero/dotnet-starter-kit/blob/3f2959e683e9f83f13e55e1678c9119f63c7e8e5/src/Modules/Files/Modules.Files/Features/v1/DeleteFile/DeleteFileCommandHandler.cs#L7-L43","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nawait client.DeleteFileAsync(id);\n// after (idempotent client handling)\ntry { await client.DeleteFileAsync(id); }\ncatch (ApiException e) when (e.StatusCode == 404) { /* already deleted — ignore */ }","handlingStrategy":"try-catch","validationCode":"var exists = await db.FileAssets.AnyAsync(x => x.Id == cmd.FileAssetId, ct);\nif (!exists) return; // nothing to delete — treat as idempotent success","typeGuard":"bool IsValidFileAssetId(Guid id) => id != Guid.Empty;","tryCatchPattern":"catch (NotFoundException) { /* idempotent delete: already gone */ return Results.NoContent(); }","preventionTips":["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."],"tags":["not-found","delete","files","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"}