Soft Delete¶
The Soft Delete module provides two strategies for non-destructive deletion: column-based flags and shadow tables. Entities implement ISoftDeletable; the runtime handles the storage strategy transparently.
Quick Start¶
using Deepstaging.SoftDelete;
// Mark an entity as soft-deletable
public class Order : ISoftDeletable
{
public OrderId Id { get; set; }
public string Description { get; set; }
public bool IsDeleted { get; set; }
public DateTimeOffset? DeletedAt { get; set; }
}
// Explicit soft-delete via service
await softDeleteService.SoftDeleteAsync(order);
Features¶
| Feature | Description |
|---|---|
ISoftDeletable |
Marker interface for soft-deletable entities |
ISoftDeleteService<T> |
Explicit operations — SoftDeleteAsync, SoftDeleteRangeAsync |
SoftDeleteStrategy |
Column (flag-based) or ShadowTable (move to parallel table) |
SoftDeleteOptions |
Strategy selection, shadow table suffix, column names |
IDeletedEntityStore |
Query deleted records for audit/recovery |
DeletedRecord |
Wraps deleted entity with deletion metadata |
Strategies¶
Column Strategy¶
Marks rows as deleted using a deleted_at column. Requires global query filters to exclude deleted rows from all queries.
orders table:
| id | description | deleted_at |
|----|-------------|---------------------|
| 1 | Active | NULL |
| 2 | Deleted | 2026-03-15 10:30:00 |
Shadow Table Strategy (default)¶
Moves deleted rows to a parallel shadow table (e.g., orders_deleted). Keeps hot tables clean with zero query-filter overhead.
orders table: orders_deleted table:
| id | description | | id | description | deleted_at | deleted_by |
|----|-------------| |----|-------------|---------------------|------------|
| 1 | Active | | 2 | Was here | 2026-03-15 10:30:00 | user-123 |
Configuration¶
| Property | Default | Description |
|---|---|---|
Strategy |
ShadowTable |
Column or ShadowTable |
ShadowTableSuffix |
"_deleted" |
Suffix for shadow table names |
DeletedAtColumn |
"deleted_at" |
Timestamp column name |
DeletedByColumn |
"deleted_by" |
Actor column name |
Core Interfaces¶
// Marker — implement on entities that support soft-delete
public interface ISoftDeletable { }
// Explicit operations (bypasses EF interceptor)
public interface ISoftDeleteService<T> where T : class, ISoftDeletable
{
Task SoftDeleteAsync(T entity, CancellationToken ct = default);
Task SoftDeleteRangeAsync(IEnumerable<T> entities, CancellationToken ct = default);
}
Deleted Entity Store¶
IDeletedEntityStore<T> provides query, restore, and purge operations for soft-deleted entities:
public interface IDeletedEntityStore<T> where T : class, ISoftDeletable
{
Task<IReadOnlyList<DeletedRecord<T>>> GetDeletedAsync(int limit = 100, CancellationToken ct = default);
Task<DeletedRecord<T>?> GetDeletedByIdAsync(object id, CancellationToken ct = default);
Task<T> RestoreAsync(object id, CancellationToken ct = default);
Task PurgeAsync(object id, CancellationToken ct = default);
Task<int> PurgeOlderThanAsync(TimeSpan age, CancellationToken ct = default);
}
DeletedRecord\<T>¶
Restore Example¶
// Restore a soft-deleted order
var restored = await deletedStore.RestoreAsync(orderId);
// Order is back in the main table, removed from shadow table
Purge Example¶
// Permanently remove soft-deleted records older than 90 days
var purged = await deletedStore.PurgeOlderThanAsync(TimeSpan.FromDays(90));
Console.WriteLine($"Purged {purged} records");
Query Deleted Records¶
// List the 50 most recently deleted orders
var deleted = await deletedStore.GetDeletedAsync(limit: 50);
foreach (var record in deleted)
{
Console.WriteLine($"{record.Entity.Id} deleted at {record.DeletedAt} by {record.DeletedBy}");
}
Source¶
src/Core/Deepstaging.Runtime/SoftDelete/