DEV Community

Javid Gahramanov
Javid Gahramanov

Posted on

Real-World .NET and EF Solutions: Simplifying Multitenancy and Soft Delete with Global Query Filters

Global query filters in Entity Framework are a powerful feature introduced in EF Core 2.0. They allow you to define filters at the model level that apply automatically to every query involving a particular entity. This eliminates the need to repeatedly include specific filter conditions in your queries. In this article, we'll explore two common use cases for global query filters in ASP.NET Core applications: Multitenancy and Soft Delete.

Why Use Global Query Filters?

Imagine you're working on a multitenant application where every entity is scoped by a TenantId. You'd typically filter queries to include only entities belonging to the current tenant. Without global query filters, you'd need to include the TenantId condition in every query, which can become repetitive and error-prone.

Another scenario is implementing Soft Delete, where instead of physically deleting records, you mark them as deleted using a flag (e.g., IsDeleted). Without a global filter, you’d need to ensure that every query excludes entities where IsDeleted = true.


Setting Up Global Query Filters in EF Core

Tenant and Softdelete Filtering
Let’s say you have an application with the following BaseEntity:

public class BaseEntity
{
    public bool IsDeleted { get; set; }

    public Guid TenantId { get; set; };
}
Enter fullscreen mode Exit fullscreen mode

Define your DbContext with a global query filter for TenantId:

public class ApplicationContext(DbContextOptions<ApplicationContext> options, IRequestContext context) : DbContext(options)
{
    public DbSet<Product> Products { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);
        modelBuilder.ApplyGlobalFilter<BaseEntity>(e => !e.IsDeleted && e.TenantId == context.TenantId);
    }
}
Enter fullscreen mode Exit fullscreen mode

Tenant specific entity - Product

public class Product : BaseEntity
{
    public string Name { get; set; }  = null!;

    public decimal Price { get; set; } 
}
Enter fullscreen mode Exit fullscreen mode

Extension to Register query filters with Convention-Based technique.

public static class ModelBuilderFilterExtensions
{
    public static void ApplyGlobalFilter<TEntity>(this ModelBuilder modelBuilder, Expression<Func<TEntity, bool>> expression)
    {
        var type = typeof(TEntity);
        var entities = modelBuilder.Model.GetEntityTypes()
            .Where(e => type.IsAssignableFrom(e.ClrType))
            .Select(e => e.ClrType);

        foreach (var entity in entities)
        {
            var param = Expression.Parameter(entity);
            var body = ReplacingExpressionVisitor.Replace(expression.Parameters.Single(), param, expression.Body);
            modelBuilder.Entity(entity).HasQueryFilter(Expression.Lambda(body, param));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

How It Works

The ApplyGlobalFilter method in ModelBuilderFilterExtensions is responsible for dynamically adding filters to all entities that inherit from a specific type (BaseEntity in this case).

When you inject the ApplicationDbContext into your services or controllers, queries for entities inheriting BaseEntity will automatically include the TenantId filter.

When GetProductsAsync is called in the ProductService, the following occurs:

The ApplicationDbContext applies the global query filters (!IsDeleted && TenantId == context.TenantId) to the Products DbSet.
Only products belonging to the current tenant and not marked as deleted are included in the query results.
This streamlined approach ensures cleaner code and enforces consistent filtering across all queries without additional effort.

public class ProductService(ApplicationDbContext dbContext)
{
    public async Task<List<Product>> GetProductsAsync()
    {
        return await dbContext.Products.ToListAsync();
        // Automatically filters by the current TenantId
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion
EF's global query filter is a fantastic tool for simplifying repetitive filtering logic in applications. Whether you're managing multitenancy with TenantId or implementing soft delete with IsDeleted, they make your code cleaner and more maintainable.

Official doc : https://learn.microsoft.com/en-us/ef/core/querying/filters

Happy coding! 🚀

Top comments (2)

Collapse
 
igor_dyatsenko profile image
Igor Dyatsenko

Great article! I’ll apply this approach in my projects.

Collapse
 
huseyn_alizada_91 profile image
Huseyn Alizada

Great article! It is really helpful