Global Query Filters in Entity Framework Core: A Complete Guide
Hey everyone, welcome back!
In this article, we’re going to look at a really useful feature in Entity Framework Core — Global Query Filters.
If you’ve ever written the same WHERE
condition over and over in your queries, you know how easy it is to forget it once, causing bugs or even exposing sensitive data.
Global Query Filters solve this problem by automatically applying those conditions across all queries.
We’ll walk through what they are, when to use them, and how they’ve been improved in .NET 10.
A Quick Shout-Out to the Sponsor
Before we continue, a quick shout-out to today’s sponsor — Entity Framework Extensions.
If you’ve worked with EF Core, you know that performance can sometimes be a challenge.
EF Extensions helps solve this by providing bulk operations like Bulk Insert, Bulk Update, and Bulk Delete, which run up to 14x faster than the default EF Core operations.
I’ll drop a link in the description so you can check it out and see how it can speed up your EF Core projects.
What Is a Global Query Filter?
In simple terms, a global query filter is a condition that EF Core automatically applies to all queries for a given entity.
Think of it like a permanent WHERE
clause that you define once in your model, and EF Core applies it everywhere automatically.
When to Use Them
Global query filters are super helpful when you have rules that should always apply.
For example:
-
Hiding soft-deleted records
-
Ensuring tenant isolation in multi-tenant apps
-
Excluding archived records from daily queries
Practical Use Cases
1. Soft Deletion
Instead of physically removing a row from the database, we mark it as deleted with an IsDeleted
flag.
Global filters then automatically hide those rows from queries.
2. Multi-Tenancy
In SaaS applications, multiple tenants share the same database, but each tenant should only see their own data.
A global filter ensures tenant isolation without you having to remember it in every query.
3. Archiving Old Data
Sometimes records need to remain in the database for compliance or reporting purposes, but we don’t want them cluttering up everyday queries.
A filter hides them by default but still keeps them accessible when needed.
Implementing Global Query Filters
Let’s see how this works in practice.
I’ve created a brand-new .NET 10 Web API project, and we’ll walk through how to implement soft deletion using global query filters.
I’ve already installed the required libraries:
-
Microsoft.EntityFrameworkCore
-
Microsoft.EntityFrameworkCore.Sqlite
-
Microsoft.EntityFrameworkCore.Tools
We’ll use SQLite as the database provider.
Creating the Blog Entity
Inside the Entities
folder, I created a sealed class called Blog
.
It includes three properties:
-
Id
-
Name
-
IsDeleted
The IsDeleted
flag will be used later for our soft delete scenario, where instead of removing a blog from the database, we’ll mark it as deleted and let the global query filter hide it automatically.
Setting Up the ApplicationDbContext
Next, in the Data
folder, I added a sealed class ApplicationDbContext
that inherits from DbContext
.
I used a primary constructor to pass options directly to the base DbContext
.
Then I added:
public DbSet<Blog> Blogs { get; set; }
This tells EF Core to create a table for Blog
objects in the database.
Adding the Global Query Filter
Now let’s override the OnModelCreating
method and add a global query filter:
modelBuilder.Entity<Blog>()
.HasQueryFilter(b => !b.IsDeleted);
This line ensures that every query EF Core generates against the Blogs
entity automatically includes a condition:WHERE IsDeleted = false
.
That means if a blog is marked as deleted, it becomes invisible in normal queries — no need to manually add the condition everywhere.
Creating API Endpoints
I’ve already set up some API endpoints:
-
GET /api/blogs
— returns all blogs (but hides deleted ones due to the filter) -
GET /api/blogs/all
— returns all blogs, including deleted ones (IgnoreQueryFilters()
) -
GET /api/blogs/{id}
— returns a single blog by ID (skipping deleted ones) -
POST /api/blogs
— creates a new blog
We’ll add the delete endpoint later to complete the soft deletion workflow.
Adding Connection String and Configurations
In appsettings.json
:
"ConnectionStrings": {
"DefaultConnection": "Data Source=Data/AppDb.db"
}
In Program.cs
:
builder.Services.AddDbContext<ApplicationDbContext>(
options => options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
Then we create the database:
Add-Migration Initial
Update-Database
This generates a Blogs
table with columns Id
, Name
, and IsDeleted
.
Testing the Endpoints
To simplify testing, I created an .http
file with sample requests for:
-
Getting blogs (with filter)
-
Getting all blogs (ignoring filter)
-
Getting by ID
-
Creating a new blog
Testing Steps
-
Call
GET /api/blogs
— returns an empty array (no blogs yet). -
Call
POST /api/blogs
— creates a new blog. -
Call
GET /api/blogs
again — returns the newly created blog. -
Call
GET /api/blogs/all
— returns the same, as no blogs are deleted yet.
Implementing the Delete Endpoint
Now let’s add DELETE /api/blogs/{id}
.
Initially, this will perform a hard delete, removing the record completely.
But we want to implement soft deletion.
To achieve that, we override SaveChangesAsync
in our context:
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
foreach (var entry in ChangeTracker.Entries<Blog>().Where(e => e.State == EntityState.Deleted))
{
entry.State = EntityState.Modified;
entry.Entity.IsDeleted = true;
}
return await base.SaveChangesAsync(cancellationToken);
}
This intercepts delete operations and converts them into soft deletes by setting IsDeleted = true
.
Testing Soft Deletion
-
Delete a blog by ID → returns
204 No Content
. -
GET /api/blogs
→ empty array (filter hides deleted record). -
GET /api/blogs/all
→ shows the deleted blog withIsDeleted = true
.
This demonstrates that soft deletion works — the data remains in the database but is invisible by default.
EF Core 10 Improvements: Named Filters
Before EF Core 10, global query filters had some major limitations:
-
Only one filter per entity was allowed.
Combining multiple rules (e.g., soft delete + tenant isolation) required a single expression. -
Using
IgnoreQueryFilters()
disabled all filters for that entity, meaning you couldn’t selectively bypass just one.
These issues made global filters somewhat inflexible.
EF Core 10 Fixes This
With named filters, you can now:
-
Define multiple filters per entity.
-
Disable individual filters selectively using:
IgnoreQueryFilters("SoftDeleteFilter");
This makes filters cleaner, modular, and more flexible, especially for applications with layered data protection.
Implementing Named Filters
First, define a static class to hold filter names:
public static class BlogFilters
{
public const string SoftDeleteFilter = "SoftDeleteFilter";
}
Then in your OnModelCreating
method:
modelBuilder.Entity<Blog>()
.HasQueryFilter(b => !b.IsDeleted)
.HasFilterName(BlogFilters.SoftDeleteFilter);
Now, in your endpoints, you can selectively disable a filter:
.IgnoreQueryFilters(BlogFilters.SoftDeleteFilter);
This gives you fine-grained control — you can ignore only the soft delete rule while keeping tenant or role-based filters active.
Testing Named Filters
-
Create a new blog.
-
GET /api/blogs
→ returns only non-deleted blogs. -
DELETE /api/blogs/{id}
→ soft deletes the blog. -
GET /api/blogs
→ hides it. -
GET /api/blogs/all
→ shows both active and deleted records.
Everything works as expected.
Conclusion
Today, we explored:
-
What Global Query Filters are in EF Core
-
How they automate rules like Soft Deletion
-
Their limitations before EF Core 10
-
The Named Filters feature that adds flexibility
-
A complete working example using .NET 10 Web API and SQLite
This feature makes your applications safer, cleaner, and easier to maintain.
Related Posts
Leave a Reply Cancel reply
Service
Categories
- DEVELOPMENT (109)
- DEVOPS (54)
- FRAMEWORKS (32)
- IT (25)
- QA (14)
- SECURITY (14)
- SOFTWARE (13)
- UI/UX (6)
- Uncategorized (8)