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.

  1. GET /api/blogs — Should return an empty array initially.

  2. POST /api/blogs — Create a new blog; returns a 201 response.

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

  4. 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 with IsDeleted = true.

EF Core 10 Improvements: Named Filters

Before EF Core 10, Global Query Filters had two main limitations:

  1. Only one filter per entity was allowed.

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