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.

AuditableEntity.csC#
public abstract class AuditableEntity<TId>
{
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
    public string? DeletedBy { get; set; }  // Admin who deleted
}
Soft-Delete Lifecycle
Soft Delete
IsDeleted = true
Hidden from normal queries
Recycle Bin (IgnoreQueryFilters)
Restore
IsDeleted = false (Visible again)
Hard delete from DB

IgnoreQueryFilters Pattern

RecycleBinRepository.csC#
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.

RecycleBinRepository.csC#
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

FeatureExecuteUpdateAsyncTraditional EF
SQL generatedSingle UPDATE ... SET ... WHEREOne UPDATE per entity
Memory usageZero entity loadingAll entities loaded to memory
Change trackerBypassedActive (overhead)
InterceptorsBypassedTriggers audit interceptor
SpeedO(1) SQL roundtripO(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 Item Restoration Flow
Admin Sends Restore Request (RestoreDeletedItemCommand)
Decrypt Encrypted ID & Resolve EntityType
Query DB with IgnoreQueryFilters() to Find Soft-Deleted Item
Reset Soft-Delete Flags (IsDeleted=false, DeletedAt/By=null)
Is Entity a Tenant?
Yes
Cascade Restore: Bulk Update Tenant's Admins, Users, Roles, Groups (ExecuteUpdateAsync)
Is Entity a UserGroup?
Yes
Restore AdminUserGroup mappings & group admins (optional)
Invalidate Admin Permission Cache & Role Cache
Save Changes and Return Success Status

Recycle Bin Endpoints

MethodEndpointDescriptionAuth
GET/api/recycle-bin/tenantsList deleted tenants🔒 Required
POST/api/recycle-bin/tenants/{id}/restoreRestore tenant + cascade children🔒 Required
DELETE/api/recycle-bin/tenants/{id}/purgePermanent hard delete🔒 Required
GET/api/recycle-bin/adminsList deleted admins🔒 Required
POST/api/recycle-bin/admins/{id}/restoreRestore admin🔒 Required

Purge vs Restore

ActionReversible?What Happens
RestoreYes (delete again)Sets IsDeleted = false, entity reappears
Purge NoHard DELETE FROM data gone forever