Skip to content

Soft Delete Pattern

Entities are marked as deleted instead of being physically removed.

Overview

stateDiagram-v2
    [*] --> Active: Create
    Active --> Active: Update
    Active --> Deleted: Soft Delete
    Deleted --> Active: Restore
    Deleted --> [*]: Cleanup Job

Soft delete preserves data for: - Audit trails - Undo functionality - Data recovery - Referential integrity

Implementation

Interface

public interface ISoftDeletable
{
    bool IsDeleted { get; set; }
    DateTime? DeletedDate { get; set; }
}

Entity

public class Marker : AuditableEntity, ISoftDeletable
{
    public Guid MarkerId { get; set; }
    public string Title { get; set; }

    // ISoftDeletable
    public bool IsDeleted { get; set; }
    public DateTime? DeletedDate { get; set; }
}

Automatic Handling

SaveChangesAsync intercepts deletes and converts to soft delete:

// In DataContext.SaveChangesAsync
foreach (var entry in ChangeTracker.Entries<ISoftDeletable>())
{
    if (entry.State == EntityState.Deleted)
    {
        entry.State = EntityState.Modified;
        entry.Entity.IsDeleted = true;
        entry.Entity.DeletedDate = dateTimeProvider.UtcNow;
    }
}

Querying

Exclude Deleted (Default)

var markers = await context.Markers
    .Where(m => !m.IsDeleted)
    .ToListAsync();

Include Deleted

var allMarkers = await context.Markers
    .IgnoreQueryFilters()
    .ToListAsync();

Restoring

marker.IsDeleted = false;
marker.DeletedDate = null;
await context.SaveChangesAsync();

Entities Using Soft Delete

Entity Soft Delete
Group Yes
Marker Yes
Note Yes
NoteItem Yes
Transaction Yes
RecurringTransaction Yes
Category Yes
BudgetAccount Yes
User No
StoredFile No
AppTask No