Simplifying Multi-Tenancy in EF Core: A Beginner’s Guide to Global Query Filters
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 🙂


