Implementing Soft Delete With EF Core
Tags: efcore, dotnet, softdelete, database
Published: 2024-03-18 11:39:32

---
title: Implementing Soft Delete With EF Core
published: true
date: 2024-03-16 00:00:00 UTC
tags: efcore,dotnet,softdelete,database
canonical_url: https://www.milanjovanovic.tech/blog/implementing-soft-delete-with-ef-core
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ego1v1lspf8977axz5ii.png
---
To delete or not to delete, that is the question (pun intended).

The traditional way to remove information in a database is through a "hard delete." A hard delete permanently erases a record from the database table. While this seems straightforward, it presents a significant risk: once that data is gone, it's gone for good.

Instead of physically removing a record, a soft delete marks it as deleted, usually by setting a flag like `IsDeleted` to `true`. The record remains in the database, but it's effectively hidden from regular application queries.

Today, we'll dive into the details of how to implement soft deletes using EF Core. We'll discuss global query filters, explore efficient ways to handle soft-deleted data, and weigh the trade-offs.

## What Is a Soft Delete?

A soft delete is a data persistence strategy that prevents the permanent deletion of records from your database. Instead of removing data from the database, a flag is set on the record, indicating it as "deleted."

This approach allows the application to ignore these records during normal queries. However, you can restore these records if necessary. Soft delete is also practical if you want to keep foreign key constraints in place. Soft delete is a "non-destructive" operation in contrast with hard delete, where data is completely removed from the database.

A hard delete uses the SQL `DELETE` statement:

```sql
DELETE FROM bookings.Reviews
WHERE Id = @BookingId;
```

A soft delete, on the other hand, uses an `UPDATE` statement:

```sql
UPDATE bookings.Reviews
SET IsDeleted = 1, DeletedOnUtc = @UtcNow
WHERE Id = @BookingId;
```

The data is still present in the database, and the operation can be undone.

But you need to remember to filter out soft-deleted data when querying the database:

```sql
SELECT *
FROM bookings.Reviews
WHERE IsDeleted = 0;
```

Let's see how we can implement soft delete with [EF Core](https://learn.microsoft.com/en-us/ef/core/).

## Soft Deletes Using EF Core Interceptors

[EF Core interceptors](https://learn.microsoft.com/en-us/ef/core/logging-events-diagnostics/interceptors)provide a powerful mechanism for intercepting and modifying database operations. For example, you can intercept the saving changes operation to implement soft delete functionality.

Let's create an `ISoftDeletable` marker interface to represent soft-deletable entities:

```csharp
public interface ISoftDeletable
{
    bool IsDeleted { get; set; }

    DateTime? DeletedOnUtc { get; set; }
}
```

The entities that should support soft delete will implement this interface. You will need to apply the respective database migration to create these columns.

The next component we need is a `SaveChangesInterceptor`, which allows us to hook into the `SavingChangesAsync` (or `SavingChanges`) method. We can access the `ChangeTracker` and look for entries that implement `ISoftDeletable` and are flagged for deletion. We can figure this out by checking if the entity state is `EntityState.Deleted`.

When we find the entities flagged for deletion, we loop through them and update their state to `EntityState.Modified`. You should also set the respective values for the `IsDeleted` and `DeletedOnUtc` properties. This will cause EF to generate an `UPDATE` operation instead of a `DELETE` operation.

```csharp
public sealed class SoftDeleteInterceptor : SaveChangesInterceptor
{
    public override ValueTask<InterceptionResult<int>> SavingChangesAsync(
        DbContextEventData eventData,
        InterceptionResult<int> result,
        CancellationToken cancellationToken = default)
    {
        if (eventData.Context is null)
        {
            return base.SavingChangesAsync(
                eventData, result, cancellationToken);
        }

        IEnumerable<EntityEntry<ISoftDeletable>> entries =
            eventData
                .Context
                .ChangeTracker
                .Entries<ISoftDeletable>()
                .Where(e => e.State == EntityState.Deleted);

        foreach (EntityEntry<ISoftDeletable> softDeletable in entries)
        {
            softDeletable.State = EntityState.Modified;
            softDeletable.Entity.IsDeleted = true;
            softDeletable.Entity.DeletedOnUtc = DateTime.UtcNow;
        }

        return base.SavingChangesAsync(eventData, result, cancellationToken);
    }
}
```

This approach ensures that all delete operations across the application respect the soft delete policy.

You'll need to register the `SoftDeleteInterceptor` with dependency injection and configure it with the `ApplicationDbContext`.

```csharp
services.AddSingleton<SoftDeleteInterceptor>();

services.AddDbContext<ApplicationDbContext>(
    (sp, options) => options
        .UseSqlServer(connectionString)
        .AddInterceptors(
            sp.GetRequiredService<SoftDeleteInterceptor>()));
```

If you want to learn more, here's an article with a few practical use cases for [EF Core interceptors](https://www.milanjovanovic.tech/blog/how-to-use-ef-core-interceptors).

## Automatically Filtering Soft-Deleted Data

To ensure that soft-deleted records are automatically excluded from queries, we can use [EF Core global query filters](https://www.milanjovanovic.tech/blog/how-to-use-global-query-filters-in-ef-core). We can apply query filters to entities using the `OnModelCreating` method to automatically exclude records marked as deleted. This feature dramatically simplifies writing queries.

Here's how to configure the soft delete query filter:

```csharp
public sealed class ApplicationDbContext(
    DbContextOptions<UsersDbContext> options) : DbContext(options)
{
    public DbSet<Review> Reviews { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Review>().HasQueryFilter(r => !r.IsDeleted);
    }
}
```

A limitation is you can't have more than one query filter configured per entity.

However, it's sometimes useful to explicitly include soft-deleted records. You can achieve this using the `IgnoreQueryFilters` method.

```csharp
dbContex.Reviews
    .IgnoreQueryFilters()
    .Where(r => r.ApartmentId == apartmentId)
    .ToList();
```

## Faster Queries Using Filtered Index

To enhance query performance, especially in tables with a significant number of soft-deleted records, you can use **filtered indexes**. A filtered index only includes records that meet the specified criteria. This reduces the index size and improves query execution times for operations that exclude filtered records. Most popular databases support filtered indexes.

Here's how you can configure a [filtered index with EF Core](https://learn.microsoft.com/en-us/ef/core/modeling/indexes?tabs=data-annotations#index-filter):

```csharp
public sealed class ApplicationDbContext(
    DbContextOptions<UsersDbContext> options) : DbContext(options)
{
    public DbSet<Review> Reviews { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Review>().HasQueryFilter(r => !r.IsDeleted);

        modelBuilder.Entity<Review>()
            .HasIndex(r => r.IsDeleted)
            .HasFilter("IsDeleted = 0");
    }
}
```

The `HasFilter` method accepts the SQL filter for records that will be included in the index.

You can also create a filtered index using SQL:

```sql
CREATE INDEX IX_Reviews_IsDeleted
ON bookings.Reviews (IsDeleted)
WHERE IsDeleted = 0;
```

You can learn more about filtered indexes from the documentation:

- [SQL Server filtered index](https://learn.microsoft.com/en-us/sql/relational-databases/indexes/create-filtered-indexes?view=sql-server-ver16)
- [PostgreSQL partial index](https://www.postgresql.org/docs/16/indexes-partial.html)

## Do You Really Need Soft Deletes?

It's worthwhile to think through if you even need to soft delete records.

In enterprise systems, you're typically not thinking about "deleting" data. There are business concepts that don't involve deleting data. A few examples are canceling an order, refunding a payment, or voiding an invoice. These "destructive" operations return the system to a previous state. But from a business perspective, you aren't really deleting data.

Soft deletes are helpful if there is a risk of accidental deletion. They allow you to easily restore soft-deleted records.

In any case, consider if soft deletes make sense from a business perspective.

## Takeaway

Soft deletes offer a valuable safety net for data recovery and can enhance historical data tracking. However, it's crucial to assess whether they truly align with your application's specific requirements. Consider factors like the importance of deleted data recovery, any auditing needs, and your industry's regulations. Creating a filtered index can improve query performance on tables with soft-deleted records.

If you decide that soft deletes are a good fit, EF Core provides the tools necessary for a streamlined implementation.

Thanks for reading, and I'll see you next week!

* * *

P.S. Whenever you're ready, there are 3 ways I can help you:

1. [**Modular Monolith Architecture:**](https://www.milanjovanovic.tech/modular-monolith-architecture?utm_source=dev.to&utm_medium=website&utm_campaign=cross-posting) This in-depth course will transform the way you build monolith systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario. [**Join the waitlist here.**](https://www.milanjovanovic.tech/modular-monolith-architecture?utm_source=dev.to&utm_medium=website&utm_campaign=cross-posting)

2. [**Pragmatic Clean Architecture:**](https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&utm_medium=website&utm_campaign=cross-posting) This comprehensive course will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture. [**Join 2,400+ students here.**](https://www.milanjovanovic.tech/pragmatic-clean-architecture?utm_source=dev.to&utm_medium=website&utm_campaign=cross-posting)

3. [**Patreon Community:**](https://www.patreon.com/milanjovanovic) Think like a senior software engineer with **access to the source code** I use in my YouTube videos and **exclusive discounts** for my courses. [**Join 1,050+ engineers here.**](https://www.patreon.com/milanjovanovic)