Hey everyone, welcome back!
In this article, we’re going to look at a really useful feature in Entity Framework CoreGlobal 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:

  1. Hiding soft-deleted records

  2. Ensuring tenant isolation in multi-tenant apps

  3. 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

  1. Call GET /api/blogs — returns an empty array (no blogs yet).

  2. Call POST /api/blogs — creates a new blog.

  3. Call GET /api/blogs again — returns the newly created blog.

  4. 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

  1. Delete a blog by ID → returns 204 No Content.

  2. GET /api/blogs → empty array (filter hides deleted record).

  3. GET /api/blogs/all → shows the deleted blog with IsDeleted = 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:

  1. Only one filter per entity was allowed.
    Combining multiple rules (e.g., soft delete + tenant isolation) required a single expression.

  2. 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

  1. Create a new blog.

  2. GET /api/blogs → returns only non-deleted blogs.

  3. DELETE /api/blogs/{id} → soft deletes the blog.

  4. GET /api/blogs → hides it.

  5. 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.