If you are building an application where multiple clients (tenants) share the same database, you are dealing with a “Multi-Tenant” architecture.

In these systems, ensuring that User A cannot see User B’s data is critical. Usually, this means every single database query you write needs a .Where(x => x.TenantId == currentTenantId) clause.

But what happens if a developer forgets to add that Where clause? A massive data leak.

Fortunately, Entity Framework (EF) Core provides an elegant, foolproof solution for this: Global Query Filters. Let’s break down what they are and how to use them, so you never have to worry about accidentally exposing the wrong data again.

What is a Global Query Filter?

A Global Query Filter is exactly what it sounds like: a LINQ query filter applied directly to an Entity type inside your DbContext. Once configured, EF Core will automatically append this filter to every LINQ query involving that entity.

It happens invisibly in the background. Your developers can just write _context.Products.ToList(), and EF Core translates it to “Get all products where the TenantId matches.”

Step-by-Step Example

Let’s look at a practical example. Imagine we are building an Inventory Management System, and we have a Product entity.

1. The Entity

Every entity that belongs to a specific tenant needs a TenantId property.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    
    // This links the product to a specific tenant
    public int TenantId { get; set; } 
}

 

2. Getting the Current Tenant

To filter by tenant, your DbContext needs to know who is currently making the request. Usually, you get this from an injected service that reads the Tenant ID from the logged-in user’s claims or the request header.

public interface ITenantService
{
    int GetCurrentTenantId();
}

 

3. Configuring the Global Query Filter in DbContext

Now, we inject that service into our DbContext and apply the filter inside the OnModelCreating method using HasQueryFilter.

public class InventoryDbContext : DbContext
{
    private readonly int _tenantId;

    public InventoryDbContext(
        DbContextOptions<InventoryDbContext> options, 
        ITenantService tenantService) : base(options)
    {
        // Grab the TenantId as soon as the context is created
        _tenantId = tenantService.GetCurrentTenantId();
    }

    public DbSet<Product> Products { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // THE MAGIC HAPPENS HERE:
        // Automatically filter products by the current TenantId
        modelBuilder.Entity<Product>().HasQueryFilter(p => p.TenantId == _tenantId);
    }
}

 

4. Querying the Data (The Easy Part!)

Now, when a fresher on your team writes a query to get products, they don’t need to think about the tenant at all.

// Developer writes this:
var products = await _context.Products.ToListAsync();

// EF Core executes this in SQL:
// SELECT * FROM Products WHERE TenantId = @currentTenantId

No extra .Where() clauses. No risk of forgetting. The code is cleaner, and the system is secure by default.

The Exception: How to Bypass the Filter

Sometimes, you do want to see all data across all tenants. For example, if you are building an Admin Dashboard for the system owner to see total overall sales.

You can easily bypass the Global Query Filter for a specific query using IgnoreQueryFilters():

// This will return ALL products in the database, ignoring the TenantId filter
var allProducts = await _context.Products
                                .IgnoreQueryFilters()
                                .ToListAsync();

 

Summary

Global Query Filters are a lifesaver for multi-tenant applications. They enforce security at the lowest level, keep your repository or service layer code incredibly clean, and remove the burden of repetitive filtering from the developer’s shoulders.

Happy Coding 🙂