Found while fixing #1411 (#1413), by reading the code.
PurgeOrphanedFilesJob and PurgeDeletedFilesJob (Files, Hangfire recurring) run without a tenant context. They don't crash the way AuditRetentionJob did, because they query FileAssets with IgnoreQueryFilters(). But:
- Tenants with a dedicated database are never purged. With no tenant resolved,
FilesDbContext connects to the default database, so only the shared database's rows (and their blobs) get cleaned. File rows in a tenant's own database stay forever.
- Storage-quota refunds are lost.
PurgeDeletedFilesJob calls quotas.RecordAsync("", ...) with an empty tenant, so the purged bytes are never credited back to the owning tenant. The code comment there admits this.
Fix
Same pattern as #1406/#1413: iterate IMultiTenantStore<AppTenantInfo>.GetAllAsync(), open a scope per tenant, set IMultiTenantContextSetter, run the purge against that tenant's FilesDbContext, and refund the quota under that tenant's id. Catch and log per tenant. Drop IgnoreQueryFilters() for the tenant filter (keep it only for soft-delete where it's needed). Add integration tests that seed purgeable files in two tenants and assert per-tenant purge and quota refund.
Found while fixing #1411 (#1413), by reading the code.
PurgeOrphanedFilesJobandPurgeDeletedFilesJob(Files, Hangfire recurring) run without a tenant context. They don't crash the wayAuditRetentionJobdid, because they queryFileAssetswithIgnoreQueryFilters(). But:FilesDbContextconnects to the default database, so only the shared database's rows (and their blobs) get cleaned. File rows in a tenant's own database stay forever.PurgeDeletedFilesJobcallsquotas.RecordAsync("", ...)with an empty tenant, so the purged bytes are never credited back to the owning tenant. The code comment there admits this.Fix
Same pattern as #1406/#1413: iterate
IMultiTenantStore<AppTenantInfo>.GetAllAsync(), open a scope per tenant, setIMultiTenantContextSetter, run the purge against that tenant'sFilesDbContext, and refund the quota under that tenant's id. Catch and log per tenant. DropIgnoreQueryFilters()for the tenant filter (keep it only for soft-delete where it's needed). Add integration tests that seed purgeable files in two tenants and assert per-tenant purge and quota refund.