Open Closed

Override ISoftDelete and IMultitenant global filters #10231


User avatar
0
pablo@ccalp.net created

I'm experiencing significant performance issues due to the soft delete and multitenant filters. Is there a way to override them and use my own user-defined function?

Queries are being generated with an OR condition that makes SQL unable to seek my index

AND (@__ef_filter__p_0 = 1 OR [IsDeleted] = 0) AND (@__ef_filter__p_2 = 1 OR [TenantId] = @tenantId)

Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

9 Answer(s)
  • User Avatar
    0
    AI-Bot created
    Support Team ABP Agent
    1. Solution
    • Short answer: Yes. Starting from ABP v8.3, you can switch ABP’s EF Core global filters (ISoftDelete, IMultiTenant, etc.) to a user-defined-function (UDF) mapping approach that removes the “(@__ef_filter__p_X = 1 OR …)” pattern and generates index-friendly predicates. You can also selectively disable filters per scope/query when needed.

    • Steps (apply in your DbContext):

      1. Indicate which entities should be filtered:
    protected override bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType)
      {
          if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)) ||
              typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)))
          {
              return true;
          }
    
          return base.ShouldFilterEntity<TEntity>(entityType);
      }
    
    1. Use UDF mapping for the filter so EF generates sargable SQL without the “OR @ef_filter...”:
    using Microsoft.EntityFrameworkCore;
      using Microsoft.EntityFrameworkCore.Metadata;
      using Microsoft.EntityFrameworkCore.Metadata.Builders;
      using Microsoft.EntityFrameworkCore.Query.SqlExpressions;
      using System.Linq.Expressions;
      using Volo.Abp.Data;
      using Volo.Abp.Domain.Entities;
      using Volo.Abp.MultiTenancy;
      using Volo.Abp.EntityFrameworkCore;
    
      public class MyProjectDbContext : AbpDbContext<MyProjectDbContext>
      {
          protected bool SoftDeleteEnabled => DataFilter?.IsEnabled<ISoftDelete>() ?? false;
          protected bool MultiTenantEnabled => DataFilter?.IsEnabled<IMultiTenant>() ?? false;
    
          public MyProjectDbContext(DbContextOptions<MyProjectDbContext> options)
              : base(options)
          { }
    
          protected override Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>(ModelBuilder modelBuilder)
          {
              var expression = base.CreateFilterExpression<TEntity>(modelBuilder);
    
              // ISoftDelete
              if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
              {
                  Expression<Func<TEntity, bool>> softDeleteFilter = e => !SoftDeleteEnabled || !EF.Property<bool>(e, "IsDeleted");
    
                  if (UseDbFunction())
                  {
                      softDeleteFilter = e => SoftDeleteUdf(((ISoftDelete)e).IsDeleted, true);
    
                      var current = this.GetService<AbpEfCoreCurrentDbContext>();
    
                      modelBuilder.HasDbFunction(typeof(MyProjectDbContext).GetMethod(nameof(SoftDeleteUdf))!)
                          .HasTranslation(args =>
                          {
                              var isDeleted = args[0];
                              var boolParam = args[1];
    
                              if (current.Context?.DataFilter.IsEnabled<ISoftDelete>() == true)
                              {
                                  // IsDeleted = 0
                                  return new SqlBinaryExpression(
                                      ExpressionType.Equal,
                                      isDeleted,
                                      new SqlConstantExpression(Expression.Constant(false), boolParam.TypeMapping),
                                      boolParam.Type,
                                      boolParam.TypeMapping);
                              }
    
                              // no WHERE fragment when disabled
                              return new SqlConstantExpression(Expression.Constant(true), boolParam.TypeMapping);
                          });
                  }
    
                  expression = expression == null
                      ? softDeleteFilter
                      : QueryFilterExpressionHelper.CombineExpressions(expression, softDeleteFilter);
              }
    
              // IMultiTenant
              if (typeof(IMultiTenant).IsAssignableFrom(typeof(TEntity)))
              {
                  Expression<Func<TEntity, bool>> tenantFilter = e =>
                      !MultiTenantEnabled || EF.Property<Guid?>(e, "TenantId") == CurrentTenant.Id;
    
                  if (UseDbFunction())
                  {
                      tenantFilter = e => TenantUdf(((IMultiTenant)e).TenantId, CurrentTenant.Id);
    
                      var current = this.GetService<AbpEfCoreCurrentDbContext>();
    
                      modelBuilder.HasDbFunction(typeof(MyProjectDbContext).GetMethod(nameof(TenantUdf))!)
                          .HasTranslation(args =>
                          {
                              var entityTenantId = args[0];
                              var currentTenantId = args[1];
    
                              if (current.Context?.DataFilter.IsEnabled<IMultiTenant>() == true)
                              {
                                  // TenantId = @CurrentTenantId
                                  return new SqlBinaryExpression(
                                      ExpressionType.Equal,
                                      entityTenantId,
                                      currentTenantId,
                                      entityTenantId.Type,
                                      ((SqlExpression)entityTenantId).TypeMapping);
                              }
    
                              return new SqlConstantExpression(Expression.Constant(true), ((SqlExpression)entityTenantId).TypeMapping);
                          });
                  }
    
                  expression = expression == null
                      ? tenantFilter
                      : QueryFilterExpressionHelper.CombineExpressions(expression, tenantFilter);
              }
    
              return expression;
          }
    
          public static bool SoftDeleteUdf(bool isDeleted, bool _)
              => throw new NotSupportedException(AbpEfCoreDataFilterDbFunctionMethods.NotSupportedExceptionMessage);
    
          public static bool TenantUdf(Guid? tenantId, Guid? currentTenantId)
              => throw new NotSupportedException(AbpEfCoreDataFilterDbFunctionMethods.NotSupportedExceptionMessage);
    
          public override string GetCompiledQueryCacheKey()
          {
              return $"{base.GetCompiledQueryCacheKey()}:{SoftDeleteEnabled}:{MultiTenantEnabled}:{CurrentTenant.Id}";
          }
    
          private static bool UseDbFunction() => true; // keep as feature flag if you need
      }
    

    Resulting SQL examples:

    • When filters enabled:

      • Soft delete: WHERE [IsDeleted] = 0
      • Tenant: WHERE [TenantId] = @tenantId
    • When disabled: no WHERE fragment is emitted for that filter. This removes the “(@__ef_filter__p_X = 1 OR …)” form and allows index seeks on IsDeleted/TenantId aligned indexes.

    • Disabling or customizing filters per scope/query:

      • Disable all soft delete (for the scope):
    using (DataFilter.Disable<ISoftDelete>())
        {
            // query without soft-delete
        }
    
    • Disable multi-tenancy (for cross-tenant maintenance, etc.):
    using (DataFilter.Disable<IMultiTenant>())
        {
            // query without tenant constraint
        }
    
    • Disable for a single LINQ operation (typical EF Core pattern):
    var q = (await _repo.GetQueryableAsync())
                .IgnoreQueryFilters(); // if you really need to bypass everything
    
    • Indexing tips:

      • Ensure indexes match the new predicates. Common patterns:
        • CREATE INDEX IX_AppTable_TenantId ON AppTable(TenantId)
        • CREATE INDEX IX_AppTable_IsDeleted ON AppTable(IsDeleted)
        • For multi-tenant tables, compound indexes often help: (TenantId, IsDeleted, <frequent column>)
    • Notes:

      • The UDF methods in C# throw NotSupportedException by design; they are only placeholders for translation.
      • Remember to include the filter state in GetCompiledQueryCacheKey to avoid wrong plan caching across different filter states.
    1. Sources
    • ABP Data Filtering and UDF-based global filters (validated):
      • https://abp.io/docs/latest/framework/infrastructure/data-filtering
    • EF Core UDF mapping (official):
      • https://learn.microsoft.com/en-us/ef/core/querying/user-defined-function-mapping
    • ABP Framework repo (reference to EF Core filter infra):
      • https://github.com/abpframework/abp

    This response was generated by AI. If it didn’t solve your problem, feel free to post a new answer — our support team will jump in to help you soon.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    pablo@ccalp.net created

    I still need help, I implemented AI's suggestion, but the entities that implement ISfotDelete are not being fetched at all

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    What is your ABP version?

    We added a new feature after 8.3.0

    see https://abp.io/community/articles/use-userdefined-function-mapping-for-global-filter-pht26l07

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    pablo@ccalp.net created

    My version is: 9.2.3

    I saw the article. Could you provide more guidance on how to override the soft delete and the multitenant filters? I followed the recommendation from the AI, but I'm still getting the OR clause.

    It might be because the sample method override starts with this:

    var expression = base.CreateFilterExpression<TEntity>(modelBuilder);

    So, it might be creating the ABP framework filters and not my custom ones. Any ideas or feedback on that?

    Thanks.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    If your package version is 9.2.3 then there should be no AND (@__ef_filter__p_0 = 1 OR [IsDeleted] = 0) query.

    Can you share a simple project?

    liming.ma@volosoft.com

    Thanks.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    pablo@ccalp.net created

    Unfortunately, I don't have the time to build one. Is it something I need to enable somehow?

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    If you haven't changed any of the abp filter code, it is enabled by default. That's why I want you to provide a project.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    pablo@ccalp.net created

    This is not a new project; it started on version 7.0.3.

    I searched everywhere in my codebase for any changes to the filter code and couldn't find anything. Could there be something new in the modules configuration that I need to add/include since then to make it work natively instead of overriding the built-in filters?

    Overriding the "CreateFilterExpression" is working now, but I would prefer not to do it.

    Thanks.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    hi

    It would be best if you could share a project that reproduces the problem.

    I will find out the reason.

    liming.ma@volosoft.com

    Thanks.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 16, 2026, 14:50
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.