Typically, soft deleting involves adding IsDeleted columns to database tables that represents the model. As the name suggests this column contains a Boolean value.

Considerations

New scenarios will be introduced when making data soft deletable. They should be considered carefully.

Two such scenarios are provided below:

Uniqueness

Re-evaluate database unique constraints and adjust as required.

On a side note. A good diagram of your database will come in handy here. I have used SketchBot.io of many occasions to create them.

Consider an Email column in a User table. Email should to be unique. What happens when a user is soft-deleted and then a 'new' user, with the same email address, is added?

An answer to this scenario is to reactivate the record with an update and replace the non-unique fields. Be aware that any foreign keys from the reactivated record primary key will be assigned to the 'new' record with the same unique keys. So choose unique keys carefully.

Soft Delete in EF Core

When soft deleting, you might be tempted to add an IsDeleted column to your table and model and update all of your queries to include a check for IsDeleted == false.

That method will definitely work, but EF Core supports a more sophisticated approach called a QueryFilter. The soft delete query filter will allow database querying as normal, but with the addition of automatically incorporating soft delete functionality.

To add a QueryFilter, that utilizes a new IsDeleted column, add the following code to the DBContext class:

First, migrate your schema to include the IsDeleted property. Next, override OnModelCreating to include the following.

protected override void OnModelCreating(ModelBuilder builder)
{
    base.OnModelCreating(builder);

    builder.Entity<MyModel>().Property("IsDeleted");
    builder.Entity<MyModel>().HasQueryFilter(m => EF.Property<bool>(m, "IsDeleted") == false);
}

Then override both SaveChanges and SaveChangesAsync.

public override int SaveChanges()
{
    UpdateSoftDeleteStatuses();
    return base.SaveChanges();
}

public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
    UpdateSoftDeleteStatuses();
    return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}

private void UpdateSoftDeleteStatuses()
{
    foreach (var entry in ChangeTracker.Entries())
    {
        switch (entry.State)
        {
            case EntityState.Added:
                entry.CurrentValues["IsDeleted"] = false;
                break;
            case EntityState.Deleted:
                entry.State = EntityState.Modified;
                entry.Reload(); // We must reload because Ef sets all FKeys to Null when deleted!!!
                entry.CurrentValues["IsDeleted"] = true;
                break;
        }
    }
}

This code intercepts any deleted queries and converts them to update queries that set the IsDeleted column to true. It also makes sure that any queries that insert any new data set the IsDeleted column to false.

EF Core will perform soft delete when you request deletion from code. Queries will ignore deleted data because the QueryFilter only retains results for records with isDeleted == false.

Accessing Deleted Records

Occasionally you may want to access the soft deleted records. This can be done by writing queries that include the IgnoreQueryFilters property. See Query Filter docs for more detail.

Note. Any SQL queries will also ignore the query filters.

Copyright © 2025 delaney. All rights reserved.