Skip to main content

Soft Deletes

Infrastructure

ISoftDelete.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;

public interface ISoftDelete
{
public bool IsDeleted { get; set; }
public DateTimeOffset? DeletedAt { get; set; }

public void Undo()
{
IsDeleted = false;
DeletedAt = null;
}
}
SoftDeleteInterceptor.cs
public class SoftDeleteInterceptor : SaveChangesInterceptor  
{
private readonly IClock _clock;

public SoftDeleteInterceptor(IClock clock)
{ _clock = clock;
}

public override ValueTask<InterceptionResult<int>> SavingChangesAsync(DbContextEventData eventData,
InterceptionResult<int> result,
CancellationToken cancellationToken = new CancellationToken())
{
if (eventData.Context is null) return ValueTask.FromResult(result);

foreach (var entry in eventData.Context.ChangeTracker.Entries())
{
if (entry is not {State: EntityState.Deleted, Entity: ISoftDelete delete}) continue;

entry.State = EntityState.Modified;
delete.IsDeleted = true;
delete.DeletedAt = _clock.GetCurrentInstant();
}

return ValueTask.FromResult(result);
}

public override InterceptionResult<int> SavingChanges(
DbContextEventData eventData,
InterceptionResult<int> result)
{
if (eventData.Context is null) return result;

foreach (var entry in eventData.Context.ChangeTracker.Entries())
{
if (entry is not {State: EntityState.Deleted, Entity: ISoftDelete delete}) continue;

entry.State = EntityState.Modified;
delete.IsDeleted = true;
delete.DeletedAt = _clock.GetCurrentInstant();
}
return result;
}
}

Configuring Entities

UserEntityConfiguration.cs
public class UserEntityConfiguration : EntityMappingConfiguration<User>
{
public override void Map(EntityTypeBuilder<User> builder)
{
builder
.HasQueryFilter(p => p.IsDeleted == false);
}
}