Soft Delete in Entity Framework Core

Today we are going to explore something I mentioned in the post about deleting data, which is when we want to delete that data but without actually removing it from the database, so that the record remains there.

 

As always, this post is part of a course, for which you have the code available on GitHub.

 

1 - What is soft delete in a database?

To understand the concept, the easiest way is to realize that when we delete a record from the database, we are completely removing it. If we try to find it again in the database, it will no longer be available.

 

A soft delete is similar but not the same. What we do is, in all our entities, we add a property called IsDeleted. This property is just a boolean that indicates whether its state is deleted or not, but of course, it's only "logically" deleted, since the record as such is still available in the database.

This action is very useful because it allows us to keep the record and, in case we delete something by mistake or due to a bug, it does not get deleted permanently. Besides, it can help provide some visibility into certain actions.

Something I like as well is to include a timestamp with when it was deleted or when it was last modified.

 

This information is purely informative, just in case one day you discover a record that is deleted and shouldn't be, you will know when it happened and can go look for more information in the relevant logs.

  • NOTE: If you want to keep track of all actions happening in your entity, I recommend that you use event sourcing or bitemporal data modeling.

 

 

2 - Implementing soft delete in entity framework core

To implement soft delete in EF Core, we need to add these properties to the entity. My personal recommendation is that ALL entities have this implemented, and therefore, it should be part of a base class:

public abstract class CursoEFBaseEntity
{
    public bool IsDeleted { get; set; }
    public DateTime DeletedTimeUtc { get; set; }
}

Then, each of your entities should inherit from this base class.

public class User : CursoEFBaseEntity
{
    ...
}
public class Wokringexperience : CursoEFBaseEntity
{
    ...
}

 

Now, what we have to do is that, when we want to delete a record, instead of using the .Remove() method, we perform an update, setting the IsDeleted=true property.

public async Task<bool> Delete(int id)
{
    User? user = await _context.Users
        .FirstOrDefaultAsync(x => x.Id == id);

    if (user == null)
        return false;

    user.IsDeleted = true;
    user.DeletedTimeUtc = DateTime.UtcNow;

    _context.Users.Update(user);
    return true;
}

If we test this now, we'll see that the record in the database has been marked as deleted:

registro eliminado

  • Note: Depending on your architecture, you may need to mark all records referencing your main entity as delete true as well. In some cases, you'll see this is needed; in others, not so much. As I said, it depends on the company/domain.

 

As you can imagine, this property is simply that, a property. Neither Entity Framework nor the database will magically hide the record if you ask for it. So, if you go to your API and ask for the corresponding Id, it'll return it with the IsDeleted property set to true.

is deleted true on api

 

For Entity Framework to hide it, we have two options.

 

The first, and most obvious, is to update all queries to add a where clause and exclude all records where isDeleted is true. This can be a real nightmare if you have many queries; you'd also have to update all includes, etc.

 

The second and most efficient option is to configure this in the OnModelCreating method of your DbContext.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>()
        .HasQueryFilter(a => !a.IsDeleted);   
}

When we use HasQueryFilter, what we are doing is adding that filter to all SQL queries that will be executed against that entity.

This option is much better, but just as we saw in the post about data seed, we can delegate this configuration to their specific classes (if you have them created):

public void Configure(EntityTypeBuilder<User> builder)
{
    builder.HasQueryFilter(a => !a.IsDeleted);
    
    builder.HasData(
        new User { Email = "[email protected]", Id = 1, UserName = "user1", IsDeleted = false},
        new User { Email = "[email protected]", Id = 2, UserName = "user2", IsDeleted = false }
    );
}

Which, of course, also works.

 

 

If we run the code and hit the same endpoint as before, we'll see that it doesn't return anything.

isdeleted working

Also, if we check the executed SQL query, we see that it has the filter specified.

entity framework filters query explalined

 

2.1 - Limitations of global filters

Keep in mind that filters may have some limitations. For example, with Entity Framework, you can execute SQL queries directly by writing them in your code. If you do that, you will be bypassing the filter.

 

Or, for example, there is another function called IgnoreQueryFilters that ignores all global filters.





I hope this post is useful to you and if you have any questions, don't hesitate to ask via Twitter or YouTube.

 

Best regards!

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café