public class Customer
{
public int Id { get; set; }
public string TenantId { get; init; }
public string Name { get; init; }
public bool IsDeleted { get; set; }
} public class MyDbContext(string tenantId) : DbContext { public DbSet<Customer> Customers { get; set; } override protected void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Customer>() .HasQueryFilter(x => x.TenantId == tenantId); } }
string tenantId = "Tenant1";
using var context = new MyDbContext(tenantId);
await context.Customers.AddRangeAsync(
[
new Customer
{
TenantId = tenantId,
Name = "John Doe"
},
new Customer
{
TenantId = tenantId,
Name = "Jane Doe",
IsDeleted = true
},
new Customer
{
TenantId = "Tenant2",
Name = "Jim Doe"
}
]);
await context.SaveChangesAsync();
foreach (Customer? customer in await context.Customers.ToListAsync()) { Console.WriteLine($"Customer: {customer.Name}, Tenant: {customer.TenantId}"); }
SELECT [c].[Id], [c].[IsDeleted], [c].[Name], [c].[TenantId]
FROM [Customers] AS [c]
WHERE [c].[TenantId] = @__P_0
modelBuilder.Entity<Customer>()
.HasQueryFilter(x => x.TenantId == tenantId);
modelBuilder.Entity<Customer>()
.HasQueryFilter(x => !x.IsDeleted);
SELECT [c].[Id], [c].[IsDeleted], [c].[Name], [c].[TenantId]
FROM [Customers] AS [c]
WHERE [c].[IsDeleted] = CAST(0 AS bit)
The workaround is,
modelBuilder.Entity<Customer>()
.HasQueryFilter(x => x.TenantId == tenantId && !x.IsDeleted);
modelBuilder.Entity<Customer>()
.HasQueryFilter("TenantFilter", x => x.TenantId == tenantId)
.HasQueryFilter("SoftDeletionFilter", x => !x.IsDeleted);
SELECT [c].[Id], [c].[IsDeleted], [c].[Name], [c].[TenantId]
FROM [Customers] AS [c]
WHERE [c].[TenantId] = @P AND [c].[IsDeleted] = CAST(0 AS bit)
// Query counts with a specific Query Filter ignored
int tenantCustomersCountIncludingDeleted = await context.Customers
.IgnoreQueryFilters(["SoftDeletionFilter"])
.CountAsync(); // 2
// Query counts with all Query Filters ignored
int allCustomersCount = await context.Customers
.IgnoreQueryFilters()
.CountAsync(); // 3