Recycle Bin
Soft-delete management with cascade restore, bulk operations, and permanent purge capabilities.
How Soft-Delete Works
All entities in SCRIPE use soft-delete. When an admin deletes a record, IsDeleted is set to true and the record is hidden from normal queries via EF Core global query filters. The record remains in the database and can be restored from the Recycle Bin.
public abstract class AuditableEntity<TId>
{
public bool IsDeleted { get; set; }
public DateTime? DeletedAt { get; set; }
public string? DeletedBy { get; set; } // Admin who deleted
}IgnoreQueryFilters Pattern
public async Task<List<Tenant>> GetDeletedTenantsAsync(CancellationToken ct)
{
return await _context.Tenants
.IgnoreQueryFilters() // Bypass soft-delete AND tenant filters
.Where(t => t.IsDeleted) // Only deleted ones
.OrderByDescending(t => t.DeletedAt)
.ToListAsync(ct);
}Cascade Restore
When restoring a tenant, all child entities (admins, users, roles, permissions) must also be restored. SCRIPE uses ExecuteUpdateAsync for bulk restoration a single SQL UPDATE statement instead of loading all entities into memory.
public async Task CascadeRestoreTenantChildrenAsync(Guid tenantId, CancellationToken ct)
{
// Bulk restore admins single SQL UPDATE, no entity loading
await _context.Admins
.IgnoreQueryFilters()
.Where(a => a.TenantId == tenantId && a.IsDeleted)
.ExecuteUpdateAsync(s => s
.SetProperty(a => a.IsDeleted, false)
.SetProperty(a => a.DeletedAt, (DateTime?)null)
.SetProperty(a => a.DeletedBy, (string?)null), ct);
// Bulk restore users
await _context.Users
.IgnoreQueryFilters()
.Where(u => u.TenantId == tenantId && u.IsDeleted)
.ExecuteUpdateAsync(s => s
.SetProperty(u => u.IsDeleted, false)
.SetProperty(u => u.DeletedAt, (DateTime?)null)
.SetProperty(u => u.DeletedBy, (string?)null), ct);
// Same for Roles, RolePermissions, AdminRoles...
}ExecuteUpdateAsync vs Traditional EF
| Feature | ExecuteUpdateAsync | Traditional EF |
|---|---|---|
| SQL generated | Single UPDATE ... SET ... WHERE | One UPDATE per entity |
| Memory usage | Zero entity loading | All entities loaded to memory |
| Change tracker | Bypassed | Active (overhead) |
| Interceptors | Bypassed | Triggers audit interceptor |
| Speed | O(1) SQL roundtrip | O(n) SQL roundtrips |
Item Restoration Pipeline & Cascading
Restoring a soft-deleted entity resets the audit deletion metadata and optionally cascades restoration to associated child records, invalidating permission caches as required.
Recycle Bin Endpoints
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /api/recycle-bin/tenants | List deleted tenants | 🔒 Required |
| POST | /api/recycle-bin/tenants/{id}/restore | Restore tenant + cascade children | 🔒 Required |
| DELETE | /api/recycle-bin/tenants/{id}/purge | Permanent hard delete | 🔒 Required |
| GET | /api/recycle-bin/admins | List deleted admins | 🔒 Required |
| POST | /api/recycle-bin/admins/{id}/restore | Restore admin | 🔒 Required |
Purge vs Restore
| Action | Reversible? | What Happens |
|---|---|---|
| Restore | Yes (delete again) | Sets IsDeleted = false, entity reappears |
| Purge | No | Hard DELETE FROM data gone forever |