Entity Framework Core: Soft Delete

I've been writing a lot of Entity Framework Code recently for a company developing an OLTP System and I'd forgotten how much you get "for free" with Entity Framework.
It really can do a lot, in complex systems like this, it's nice to not care about the complexity of the data layer, instead focus on the problems at hand - the business problems.

We've been given "global" functionality to help us when querying our data with Soft Deletes, however we haven't been given the same for saving Soft Delete-able data.

Global Query Filters

The code already makes great use of Global Query Filters - in the case of soft deletes.

There's some code in the DbContext method OnModelCreating, which takes care of scanning the assembly for types which are marked to have the Soft Delete functionality (Interface = IIsDeleted).

The query filter looks a little like this pseudo code...

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var typesSoftDelete = GetAllTypesWhichImplementIIsDeleted();
    foreach (var typ in typesSoftDelete)
    {
        modelBuilder.Entity<typ>().HasQueryFilter(p => !p.IsDeleted);
    }
}

Nice and simple, whenever I'm trying to query an object from the Db, only include the ones where IsDeleted = false (they haven't been deleted)

Soft Deletes

Soft deletes aren't really deletes. They're not deleted from the database, instead a flag (IsDeleted) is marked on the specific row. So whenever we're querying for data, we make sure to exclude records where IsDeleted = true.

This allows the database to keep a history of records, e.g. companies may need to keep for compliance purposes.

We've got the global functionality for the reading of entities. However if we call .Remove(entity) or .RemoveRange(entities) then they will get deleted from the database.
Instead of using the methods above, the codebase is littered with the following when an object is to be Soft Deleted:

public void Delete(Post post)
{
    post.IsDeleted = true;
    context.Update(post);
    context.SaveChanges();
}

It works, but it's verbose. Also, new devs joining the team may not know about soft deletes. They'll write some delete code (using Remove) and cost the company some money in compliance failures when loads of the data goes missing.

Solution

I've had a good look round and taken some inspiration from some sources on the internet.

To have Entity Framework perform the Soft Deletes for us when using the remove methods mentioned above, we have to override the SaveChanges method on the DbContext.

I've created an Interface which makes it easy to Identify items which need to be Soft Deleted

public interface IIsDeleted
{
    bool IsDeleted { get; set; }
}

Any Soft-deletable Db entity should implement that interface

We then override SaveChanges like so and it's actually quite simple:

/// <summary>
/// Marks any "Removed" Entities as "Modified" and then sets the Db [IsDeleted] Flag to true
/// </summary>
private override int SaveChanges()
{
  ChangeTracker.DetectChanges();

  var markedAsDeleted = ChangeTracker.Entries().Where(x => x.State == EntityState.Deleted);

  foreach (var item in markedAsDeleted)
  {
    if (item.Entity is IIsDeleted entity)
    {
      // Set the entity to unchanged (if we mark the whole entity as Modified, every field gets sent to Db as an update)
      item.State = EntityState.Unchanged;
      // Only update the IsDeleted flag - only this will get sent to the Db
      entity.IsDeleted = true;
    }
  }
  return base.SaveChanges();
}

The override above, instructs EF to figure out what's changed on the context (if you've added, modified or removed anything).

Then we find all the entities in the context which have been marked for deletion (entities we've used .Remove(entity) or .RemoveRange(entities) on)

For each of those entities, if they're Soft-Deletable then we mark their state as Unchanged, this tells EF that they're tracked, but nothing has been modified. Then we set the IsDeleted flag to true (modifying them). This ensures that only this update gets sent to the Db (if you examine the SQL it sends to the database, it's a simple update, with a single Set \[IsDeleted] = True

Previously I was marking the whole entity as modified, then setting the IsDeleted flag, this resulted in EF Core sending all the fields in the update statement - which is a bit too chatty. By marking the entity as unchanged, then changing one field we keep the SQL update statement small and constrained to only that field

One final thing

I've only overridden one of the SaveChanges methods. There are others (SaveChangesAsync) - so just make sure those are overriden too if they're being used in the codebase.

Hope you enjoyed this article, let me know in the comments if you're using something similar or have any improvements on the above.

ryansouthgate

Software developer, living in Coventry, loves .Net, JavaScript and learning new languages.

Coventry