Multi-tenant Architecture with Global Query Filters in Entity Framework Core
One of the most common architectural models in modern SaaS applications is keeping data for all customers (tenants) in the same database but isolated logically. The biggest risk here is a developer forgetting to add a "TenantId" filter in a query, leading to data leaks. Global Query Filters provide a centralized safety net to eliminate this risk.
1. Defining the Tenant Interface
The first step is creating an interface to mark all tenant-specific entities. This allows us to automatically apply filters to the correct tables during configuration.
public interface ITenantEntity
{
Guid TenantId { get; set; }
}
2. Tenant Identification via Service
We need a service that identifies the current tenant ID from headers, subdomains, or claims for every incoming request. This ID will be injected into our DbContext.
3. Configuring the Global Filter in DbContext
Within OnModelCreating, we configure EF Core to automatically append a WHERE clause to every query executed against entities that implement ITenantEntity.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// Apply the filter to all entities implementing ITenantEntity
modelBuilder.Entity<Product>().HasQueryFilter(p => p.TenantId == _currentTenantId);
}
4. Bypassing Filters (IgnoreQueryFilters)
If you need to query across all tenants (e.g., for global analytics or an admin dashboard), you can bypass the filter explicitly using the IgnoreQueryFilters() method:
var globalInventory = await _context.Products
.IgnoreQueryFilters()
.ToListAsync();
Conclusion
Using Global Query Filters minimizes human error and makes your codebase cleaner and more secure. However, keep in mind that these filters do not apply to raw SQL queries (FromSqlRaw), so extra caution is needed when bypassing the LINQ provider.