Soft Deletes with Entity Framework Core 2 - Part 1

Entity Framework Core 2, already covered here, includes a cool feature called global filters. By leveraging global filters, we can apply restrictions automatically to entities, either loaded directly or through a collection reference. If we add this to shadow properties (in the case of relational databases, columns that exist in a table but not on the POCO model), we can do pretty cool stuff.

In this example, I am going to create a soft delete global filter to all entities in the model that implement a marker interface ISoftDeletable.

public interface ISoftDeletable
{
}
We just need to override the DbContext’s OnModelCreating method to automatically scan all known entities to see which implement this interface and then create the restriction automatically:
private const string _isDeletedProperty = "IsDeleted";
private static readonly MethodInfo _propertyMethod = typeof(EF).GetMethod(nameof(EF.Property), BindingFlags.Static | BindingFlags.Public).MakeGenericMethod(typeof(bool));

private static LambdaExpression GetIsDeletedRestriction(Type type)
{
var parm = Expression.Parameter(type, "it");
var prop = Expression.Call(_propertyMethod, parm, Expression.Constant(_isDeletedProperty));
var condition = Expression.MakeBinary(ExpressionType.Equal, prop, Expression.Constant(false));
var lambda = Expression.Lambda(condition, parm);
return lambda;
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var entity in modelBuilder.Model.GetEntityTypes())
{
if (typeof(ISoftDeletable).IsAssignableFrom(entity.ClrType) == true)
{
entity.AddProperty(_isDeletedProperty, typeof(bool));

modelBuilder
.Entity(entity.ClrType)
.HasQueryFilter(GetIsDeletedRestriction(entity.ClrType));
}
}

base.OnModelCreating(modelBuilder);
}
So, for each entity known from the context we add a shadow property called IsDeleted of type bool. Of course, needless to say, it must also exist on the database. The reason I’m making it a shadow property is to avoid people tampering with the entities, by setting or unsetting its value. This way, the restriction is always performed and it is invisible to us. After we create the property, we add a restriction to the entity’s type.
Simple, don’t you think? This way, if you want to enable or disable it for a number of entities, just have them implement the ISoftDeletable interface.

                             

10 Comments

  • Nice! But as far as I can tell, it doesn't prevent hard deletion... Is there a way to make it so that when you delete an entity, it actually updates the IsDeleted property?

  • tnx. realy usefull for me.

  • tnx. realy usefull for me.

  • I posted a similar post on my blog a few months ago. My implementation of Soft Delete using EF Core also *rewrite* deletes, so it prevents hard delete. This should answer Thomas levesque's question!
    https://www.meziantou.net/2017/07/10/entity-framework-core-soft-delete-using-query-filters

  • Hi Meziantou and Thomas Levesque! I wrote a second post on this: https://weblogs.asp.net/ricardoperes/soft-deletes-with-entity-framework-core-2-part-2. What do you think?

  • Meziantou: the only difference from your code and mine is that mine is dynamically applied, eg, I don't apply it on an entity by entity basis. ;-)

  • I would recommend to add an SQL index on the IsDeleted property. This can easily be added in OnModelCreating:

    modelBuilder.Entity(entity.ClrType).HasIndex(_isDeletedProperty).HasName($"IX_{_isDeletedProperty}");

  • Hi, Carsten! Thanks!

  • Hi,
    We implemented a soft deletion solution, similar to this using shadow properties (nullable datetime named "DeletedOn") which we check for null zu filter the deleted ones in a global filter.

    This works fine, except in this scenario:
    When we update an item which has children and on child is deleted, the DeletedOn date is set. In the same method, the data is queried again and the updated item is returned to the caller. The item contains all updated properties, but it still contains the deleted child. only in the next call (with a new context), the filter is filtering the child correctly.

    What could be the cause of that?
    Also see: https://stackoverflow.com/questions/52437870/entity-framework-core-global-query-filter-not-filtering-correctly-after-data-ch

    Thanks, Marcel

  • I would recommend to add an SQL index on the IsDeleted property. This can easily be added in OnModelCreating:

Add a Comment

As it will appear on the website

Not displayed

Your website