11

I am trying to get to work soft delete behaviour in EF Core 2.0.

public interface ISoftDeleteModel
{
    bool IsDeleted { get; set; }
}

Creating proper column and soft-deleting are working fine but filtering entities from DbContext isn't.

I would like to use query filtering in context but I am stuck.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    Type entityType;
    // ^^^ it contains type of entity, eg. Blog, Post, etc. using
    // modelBuilder.Model.GetEntityTypes().First().Name and converting to Type

    var entity = modelBuilder.Entity(entityType);
    if(entityType.GetInterface("ISoftDeleteModel") != null)
    {
        // ??? how to access IsDeleted property ???
        entity.HasQueryFilter(x => !x.IsDeleted);
    }
}

The question is simple - how to access IsDeleted property?

If I knew type of the entity, eg. Post, and Post implemented ISoftDeleteModel I would be able to do this:

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

But I do not know the type. I am trying to achieve simple thing - all models implementing this interface would be automatically filtered. Am I missing something?

abatishchev
  • 98,240
  • 88
  • 296
  • 433
Tom C.
  • 165
  • 1
  • 10

1 Answers1

30

Can't test the exact API, but the general approach would be to create a constrained generic method and call it via reflection:

public static class EFFilterExtensions
{
    public static void SetSoftDeleteFilter(this ModelBuilder modelBuilder, Type entityType)
    {
        SetSoftDeleteFilterMethod.MakeGenericMethod(entityType)
            .Invoke(null, new object[] { modelBuilder });
    }

    static readonly MethodInfo SetSoftDeleteFilterMethod = typeof(EFFilterExtensions)
               .GetMethods(BindingFlags.Public | BindingFlags.Static)
               .Single(t => t.IsGenericMethod && t.Name == "SetSoftDeleteFilter");

    public static void SetSoftDeleteFilter<TEntity>(this ModelBuilder modelBuilder) 
        where TEntity : class, ISoftDeleteModel
    {
        modelBuilder.Entity<TEntity>().HasQueryFilter(x => !x.IsDeleted);
    }
}

Now you can use something like this inside your OnModelCreating:

foreach (var type in modelBuilder.Model.GetEntityTypes())
{
    if (typeof(ISoftDeleteModel).IsAssignableFrom(type.ClrType))
        modelBuilder.SetSoftDeleteFilter(type.ClrType);
}
johnny 5
  • 19,893
  • 50
  • 121
  • 195
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343
  • I will have to read carefully and try to understand your answer. But it works. Thanks – Tom C. Jul 14 '17 at 08:52
  • Hi, please how am I supposed to edit that method in order to make it work within an inheritance scenario, with the `ISoftDeleteModel` applied to the base abstract type? Thanks. – m.phobos Dec 07 '17 at 10:51
  • @m.phobos Hi, by inheritance scenario do you mean EF inheritance (TPH) or just class inheritance? – Ivan Stoev Dec 07 '17 at 12:07
  • @m.phobos No problem, and sorry for the delay. Here is the modified `if` condition: `typeof(ISoftDeleteModel).IsAssignableFrom(type.ClrType) && (type.BaseType == null || !typeof(ISoftDeleteModel).IsAssignableFrom(type.BaseType.ClrType))` – Ivan Stoev Dec 08 '17 at 08:07
  • How to pass parameter to the filter? My filter needs TenantId and I cannot figure out how to pass it to the filter. – Marek Urbanowicz Feb 07 '18 at 21:49
  • @IvanStoev - specifically I need to pass Guid to the filter method – Marek Urbanowicz Feb 07 '18 at 22:11
  • @MU Create a property TenantId in your db context and use it inside the filter. See [EF Core 2.0.0 Query Filter is Caching TenantId (Updated for 2.0.1+)](https://stackoverflow.com/questions/47268072/ef-core-2-0-0-query-filter-is-caching-tenantid-updated-for-2-0-1/47270953#47270953) and [Model-level query filters](https://learn.microsoft.com/en-us/ef/core/what-is-new/#model-level-query-filters) – Ivan Stoev Feb 08 '18 at 01:17
  • I posted comment there - it is still not clear... Appreciate any help – Marek Urbanowicz Feb 08 '18 at 06:58
  • 1
    @IvanStoev sorry for the delay, I've been busy and couldn't test your solution before. I've changed the `if` condition as per your suggestion. I'm still having a problem while applying a migration. The `Add-Migration` command fails with this error: `The filter expression 'x => Not(x.IsDeleted)' cannot be specified for entity type ''. A filter may only be applied to the root entity type in a hierarchy.` Please do you have any further suggestion? Am I missing something else? Thank you! – m.phobos Feb 12 '18 at 14:21
  • @m.phobos You can't debug the migration, but you can run some test code (like `var db = new YourDbContext(); db.SomeTable.Count();` ) , put a breakpoint and see why the condition is hit for ``. The only reason I see from the code is if the very base entity (the root of the TPH) is not implementing the interface, but the concrete derived class does implement it. In which case you need to move the interface implementation at the root entity. – Ivan Stoev Feb 12 '18 at 14:40
  • 2
    Note that when you call `HasQueryFilter` multiple times on same table / entity type then only the latest call will have effect. If you need to chain multiple query filters, then you can use the snippet from here: https://github.com/dotnet/efcore/issues/10275#issuecomment-457504348 – Roland Pihlakas Apr 28 '20 at 18:30
  • @RolandPihlakas That's a different issue addressed here https://stackoverflow.com/questions/51497089/combine-binaryexpression-and-expressionfuncdynamic-bool-in-c-sharp/51526191#51526191 – Ivan Stoev Apr 28 '20 at 19:10
  • @IvanStoev I do understand it is a different issue. That is why I marked it as a "note". I am telling that beware that solving this issue here may reasonably likely cause a new unexpected issue in certain cases ("If you need to chain multiple query filters"). In such case both the danger that one needs to be aware of and the solution is in my posted link. – Roland Pihlakas Apr 28 '20 at 22:33