Soft Deletes with Entity Framework Core 2 - Part 1

Update: see part two here.

Entity Framework Core 2, already covered here, includes a cool feature called global query 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<T> that is generic, and another one, that is not, but inherits from it with a bool generic parameter.

public interface ISoftDeletable<T>
{
}

public interface ISoftDeletable : ISoftDeletable<bool>
{
}
We'll seen in a couple posts whats the usage of the generic version!
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);
}
In this case, only the non-generic version is used, we'll see on a next post what can we do with the generic one.
So, for each entity known from the context we add a shadow property (one that does not exist in the physical model) 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 one of the ISoftDeletable interfaces.

                             

16 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:

  • What a nice article! Finally found a decent article for what I need!

    Where should I put default value for the property? I want to add several more properties not only IsDeleted, but can't figure out where to put default values for the properties (like CreatedDate).

  • Hi, Viktoras!
    Please see part two: https://weblogs.asp.net/ricardoperes/soft-deletes-with-entity-framework-core-2-part-2.

  • Thank you Ricardo! I understand that I can check if this is the added entity and the add values with CurrentValue. But what about SQL Server functions, like SUSER_SNAME()?

  • Viktoras: for that you either need to use SQL on the SaveChanges (a bad approach) or set a default value for the column, in OnModelCreating:

    modelBuilder.Entity<MyEntity>().Property(x => x.MyProperty).HasDefaultValueSql("SUSER_SNAME()");

  • Hi! Thanks for the great post. Im having some troubles when implementing this, and i think it has to do with that I am using EF version 6, and you are using Core?

    The troubles i have are 2 different things:

    1) In the MethodInfo _propertyMethod, Visual Studio doesnt seem to recognize "EF", and it gives me an error. What is EF?

    2) Your for loop does not work for me. I instead tried something in the likes of this, but here i cannot use entity.AppProperty, it doesnt exist in this context.


    modelBuilder.Types().Configure(entity =>
    {
    if (typeof(ISoftDeletable).IsAssignableFrom(entity.ClrType) == true)
    {
    entity.AddProperty(_isDeletedProperty, typeof(bool));

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


    Any help in greatly appreachiated!

  • Sigge: this only applies to .NET Core. I do not know of a way to do this with EF "classic" and I have been away from it for a long time. Sorry! In any case, you should probably migrate to EF Core as it also works with .NET "classic".

Add a Comment

As it will appear on the website

Not displayed

Your website