Mastering Global Query Filters in Entity Framework Core 10
In today’s tutorial, we’re going to take a deep dive into one of the most powerful features of Entity Framework Core (EF Core) — Global Query Filters.
If you’ve ever found yourself writing the same WHERE
condition over and over in your queries, you know how easy it is to forget it once — and that can lead to bugs or even expose sensitive data. Global Query Filters solve this problem by automatically applying those conditions across all queries in your application.
In this article, we’ll explore what Global Query Filters are, when to use them, how to implement them, and what improvements EF Core 10 brings with named filters. We’ll also walk through a complete implementation example using .NET 10 and SQLite.
What Are Global Query Filters?
In simple terms, a Global Query Filter is a condition that EF Core automatically applies to every query for a specific entity. Think of it as a permanent WHERE
clause — defined once in your model and applied everywhere by EF Core.
This is extremely useful when you have rules that should always apply to your data. Common examples include:
-
Soft deletion — hiding records marked as deleted.
-
Multi-tenancy — ensuring that each tenant in a SaaS app only sees their own data.
-
Archiving — keeping old records in the database but excluding them from regular queries.
Practical Use Cases
1. Soft Deletion
Instead of physically removing rows from your database, you mark them as deleted with a Boolean flag such as IsDeleted = true
. Global Query Filters then automatically hide those records from all queries.
2. Multi-Tenancy
In multi-tenant applications, multiple organizations share the same database. A global filter ensures tenant isolation so that each tenant can only access their own data without manually adding filters in every query.
3. Archiving
Sometimes, you need to retain historical or compliance-related data without showing it in everyday results. A global filter hides these archived records by default while still allowing access when necessary.
Implementing Global Query Filters in .NET 10
Let’s walk through how to implement soft deletion using EF Core Global Query Filters in a brand-new .NET 10 Web API project.
Step 1: Setting Up the Project
We’ve already created a new .NET 10 Web API project and installed the required EF Core libraries, including:
-
Microsoft.EntityFrameworkCore
-
Microsoft.EntityFrameworkCore.Sqlite
-
Microsoft.EntityFrameworkCore.Tools
SQLite is used as the database provider, allowing EF Core to communicate with a local SQLite database.
Step 2: Creating the Entity
Create a folder named Entities and add a sealed class called Blog
with the following properties:
public sealed class Blog
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public bool IsDeleted { get; set; }
}
The IsDeleted
flag will be used later to mark a blog as deleted instead of physically removing it.
Step 3: Setting Up the DbContext
Inside the Data folder, create a sealed class ApplicationDbContext
inheriting from DbContext
.
Use a primary constructor to pass the options to the base class.
Add a DbSet<Blog>
property so EF Core knows to create a Blogs
table in the database.
Then, override the OnModelCreating
method and add the Global Query Filter:
modelBuilder.Entity<Blog>()
.HasQueryFilter(b => !b.IsDeleted);
This ensures that every query EF Core runs against Blogs
automatically excludes soft-deleted records.
Step 4: Setting Up API Endpoints
We’ll add several API endpoints for managing blogs:
-
GET /api/blogs
— Returns all blogs (excluding soft-deleted ones). -
GET /api/blogs/all
— Returns all blogs, including deleted ones, by calling.IgnoreQueryFilters()
. -
GET /api/blogs/{id}
— Returns a single blog by ID. -
POST /api/blogs
— Creates a new blog. -
DELETE /api/blogs/{id}
— Soft deletes a blog (we’ll configure this next).
Step 5: Configuring SQLite and Dependency Injection
In the appsettings.json, add:
"ConnectionStrings": {
"DefaultConnection": "Data Source=Data/AppDb.db"
}
Then, register the DbContext in Program.cs:
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
Step 6: Running Migrations
Run the following commands in the Package Manager Console:
Add-Migration Initial
Update-Database
This will create the SQLite database and generate the Blogs
table with the columns Id
, Name
, and IsDeleted
.
Testing the API
Use an .http
file or Postman to test the API endpoints.
-
GET /api/blogs — Should return an empty array initially.
-
POST /api/blogs — Create a new blog; returns a 201 response.
-
GET /api/blogs again — Returns the newly created blog.
-
GET /api/blogs/all — Shows all blogs (identical results for now).
Implementing Soft Deletion
Now, let’s modify the delete logic to perform soft deletion instead of a hard delete.
Override SaveChangesAsync
in ApplicationDbContext
:
public override 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 base.SaveChangesAsync(cancellationToken);}
Now, when a delete request is made, the record isn’t removed — it’s simply marked as deleted.
After performing a delete:
-
GET /api/blogs
→ returns an empty array. -
GET /api/blogs/all
→ shows the deleted record withIsDeleted = true
.
EF Core 10 Improvements: Named Filters
Before EF Core 10, Global Query Filters had two main limitations:
-
Only one filter per entity was allowed.
-
Using
.IgnoreQueryFilters()
disabled all filters on that entity.
That meant you couldn’t selectively disable just one filter (like soft delete) while keeping another (like tenant isolation).
Named Filters to the Rescue
EF Core 10 introduces Named Filters, allowing you to:
-
Define multiple filters per entity.
-
Disable only specific filters when needed.
For example:
modelBuilder.Entity<Blog>()
.HasQueryFilter("SoftDeleteFilter", b => !b.IsDeleted);
And to bypass only that filter:
context.Blogs.IgnoreQueryFilters("SoftDeleteFilter");
This gives you fine-grained control and makes your filters modular, cleaner, and more flexible — especially for multi-tenant or security-sensitive systems.
Using Constants for Filter Names
To avoid typos and keep the code organized, define your filter names in a static class:
public static class BlogFilters
{
public const string SoftDeleteFilter = "SoftDeleteFilter";
}
Then reference it like this:
.IgnoreQueryFilters(BlogFilters.SoftDeleteFilter);
This way, if you ever rename a filter, you only update it in one place.
Final Testing
After implementing named filters, run the API again:
-
GET /api/blogs
— Shows only active blogs. -
POST /api/blogs
— Adds a new blog. -
GET /api/blogs/all
— Bypasses only the soft delete filter, showing both active and deleted blogs.
This demonstrates how EF Core’s global query filters automatically manage visibility rules while still letting you override them when needed.
Conclusion
Global Query Filters are a powerful EF Core feature that simplify data access and help enforce consistency across your application.
In this walkthrough, we’ve covered:
-
What global query filters are.
-
How to implement them for soft deletion.
-
How EF Core 10’s Named Filters make them more flexible.
By using these filters, you can reduce repetitive code, enforce security, and make your application cleaner and safer.
Related Posts
Leave a Reply Cancel reply
Service
Categories
- DEVELOPMENT (111)
- DEVOPS (54)
- FRAMEWORKS (34)
- IT (25)
- QA (14)
- SECURITY (14)
- SOFTWARE (13)
- UI/UX (6)
- Uncategorized (8)