Soft Deleting in Entity Framework Core

Deleting data in any software system can be a tricky problem. Often, instead of deleting data and permanently losing it, it may be preferable to keep the data, but make it invisible to users. This practice allows for better system auditability, and it is often referred to as “soft deleting.”

Note: There are many privacy and data ownership considerations to be made when storing and managing data. I’m going to intentionally ignore those in this post, but I want to recognize that these are important issues that should be considered if you choose to implement soft delete instead of hard delete functionality.

Typically, soft deleting involves something along the lines of adding an isDeleted column to the database table that represents the model you wish to soft delete. This column usually contains either a Boolean value or a timestamp representing when the data was deleted.

Considerations

When making data soft deletable, you may introduce new scenarios that should be considered carefully.

Delete vs. deactivate

One situation you’ll need to address is how to handle new data. Suppose you have a Users table that you wish to make soft deletable. How should the system behave when a user is deleted, and then added again in the future? Often, a reasonable way to handle this is to write a new user to the database, allowing the new fields to be identical to the previously deleted ones. With this approach, the new user is completely independent and separate from the older, deleted user.

However, in other situations, this may not be desirable. Instead, it may make sense to support deactivating behavior instead of deleting. Here, if data is deleted and then re-added, the old data is simply marked as active again. This approach would retain any data relationships when the data is reactivated.

Uniqueness

If you choose to allow data to be soft deleted, you may need to adjust database uniqueness constraints and/or indexes. For example, you can’t require the email column in your Users table to be unique if you allow users to be soft-deleted, then allow new users with the same email as previously deleted users to be added.

Soft Delete in EF Core

Let’s shift gears and talk about Entity Framework 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, or something similar.

That approach will definitely work, but EF Core also supports a more sophisticated approach by adding what’s called a QueryFilter. Adding a soft delete query filter will allow you to query your database as you would normally, then automatically incorporate your soft delete functionality.

For this example, let’s imagine you choose to implement soft deleting by adding an isDeleted Boolean property to an entity called MyModel. To add a QueryFilter that utilizes this new column, you’ll need to do the following in your DBContext class:

First, migrate your schema to include the isDeleted property. Next, you’ll need to override OnModelCreating to include the following:

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

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

Then, you’ll need to override both SaveChanges and SaveChangesAsync and add this:

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.CurrentValues["isDeleted"] = true;
                break;
        }
    }
}

The above code intercepts any delete 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.

With the above changes, you can pretend that the data is actually being deleted when you write your EF Core code. Queries that read data will ignore deleted data because the QueryFilter only retains results for entities with isDeleted == false.

Accessing the Data

You may occasionally want to access the soft deleted data. To do so, you can write queries that include the IgnoreQueryFilters property. This is described in more detail in the Query Filter docs.

Additionally, any raw SQL you write will also ignore the query filters you have placed.

This is one approach for soft deleting data using Entity Framework Core. If you have any alternative approaches or thoughts about the approach I’ve presented, please share them below.