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.