🚀 New: The .NET Senior PlaybookSave 20% with launch discount 

newsletter

Named Global Query Filters Were Updated in EF Core 10

Download source code
4 min read

Newsletter Sponsors

Copied

Deploy Global .NET + Postgres Apps Without Cloud Complexity (Sponsored)

Running a .NET app in the cloud shouldn't feel hard. But many teams still deal with region setup, config files, and ongoing ops just to stay online.
Fly.io removes that friction and lets you focus on shipping features.

Screenshot_1

Fly.io runs your ASP .NET apps close to users in 35 global regions, giving fast response times and a smooth user experience worldwide. It's built for stateful .NET apps, so your API, frontend, and database can live together instead of being split across services.

With Fly.io Managed Postgres, you get automatic backups, high availability, scaling, monitoring, and encryption out of the box. No database babysitting. No late-night alerts. Just Postgres that works in production.

Pricing is predictable, and small instances make it easy to start with side projects and grow when traffic picks up. If you want simpler deploys, global reach, and less ops work for your .NET + Postgres apps, Fly.io is a solid choice.

👉 Deploy your app in 5 minutes with Fly.io

Global query filters in Entity Framework Core (EF Core) is a powerful feature that can be effectively used to manage data access patterns.

Global query filters are LINQ query predicates applied to EF Core entity models. These filters are automatically applied to all queries that involve the corresponding entities. This is especially useful in multi-tenant applications or scenarios requiring soft deletion.

In EF Core 10, Global Query Filters were renamed to Named Query Filters.

This update now solves a significant limitation EF Core previously had: support for multiple global query filters on a single entity.

In this post, we will explore:

  • Query Filters for a Soft Delete use case
  • Query Filters for a Multi-Tenant application
  • Named Query Filters

Let's dive in.

Copied

Query filters for a soft delete use case

Let's explore a use case where global query filters are particularly useful - entity soft deletion. In some applications, entities can't be completely deleted from the database and should remain for statistics and historical purposes. Or to ensure that related data remains unchanged, i.e, referenced by foreign keys. A solution for this use case is soft deletion.

Soft deletion is implemented by adding an is_deleted column to the database table for required entities. Whenever an entity is considered deleted, this column is set to true. In most application database queries, "deleted" entities should be ignored in read operations and not be visible to end users.

Let's explore an example for the following entities:

csharp
public class Author { public required Guid Id { get; set; } public required string Name { get; set; } public required string Country { get; set; } public required List<Book> Books { get; set; } = []; } public class Book { public required Guid Id { get; set; } public required string Title { get; set; } public required int Year { get; set; } public required bool IsDeleted { get; set; } public required Guid TenantId { get; set; } public required Author Author { get; set; } }

We need to create and set up our DbContext with a global query filter:

csharp
public class ApplicationDbContext : DbContext { public DbSet<Author> Authors { get; set; } = default!; public DbSet<Book> Books { get; set; } = default!; public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Book>() .HasQueryFilter(x => !x.IsDeleted); base.OnModelCreating(modelBuilder); modelBuilder.Entity<Author>(entity => { entity.ToTable("authors"); entity.HasKey(x => x.Id); entity.HasIndex(x => x.Name); entity.Property(x => x.Id).IsRequired(); entity.Property(x => x.Name).IsRequired(); entity.Property(x => x.Country).IsRequired(); entity.HasMany(x => x.Books) .WithOne(x => x.Author); }); modelBuilder.Entity<Book>(entity => { entity.ToTable("books"); entity.HasKey(x => x.Id); entity.HasIndex(x => x.Title); entity.Property(x => x.Id).IsRequired(); entity.Property(x => x.Title).IsRequired(); entity.Property(x => x.Year).IsRequired(); entity.Property(x => x.IsDeleted).IsRequired(); entity.HasOne(x => x.Author) .WithMany(x => x.Books); }); } }

In this code, we're applying a query filter to the Book entity on the IsDeleted property:

csharp
modelBuilder.Entity<Book>() .HasQueryFilter(x => !x.IsDeleted);

Here we are filtering out all softly deleted books from the result query. When querying books from DbContext, this query filter is applied automatically. Let's have a look at the following minimal API endpoint:

csharp
app.MapGet("/api/books", async (ApplicationDbContext dbContext) => { var nonDeletedBooks = await dbContext.Books.ToListAsync(); return Results.Ok(nonDeletedBooks); });

Every time we query books, we only get those that are not deleted, thus we don't need to use a LINQ Where statement in all DbContext queries.

In some cases, however, we might need to access all entities and ignore the query filter. EF Core has a special method called IgnoreQueryFilters for such a case:

csharp
app.MapGet("/api/all-books", async (ApplicationDbContext dbContext) => { var allBooks = await dbContext.Books .IgnoreQueryFilters() .Where(x => x.IsDeleted) .ToListAsync(); return Results.Ok(allBooks); });

That way, all the books are retrieved from the database, and the query filter on the Book entity is completely ignored.

Copied

Query filters for a multi-tenant application

Another practical use case for global query filters is multi-tenancy. A multi-tenant application shares a single software instance across multiple customers. All customer data should not be visible to other customers.

Let's explore the simplest implementation of multi-tenancy: storing all data in a single database and table.

First, we need to add a TenantId property to the Books entity:

csharp
public class Book { // Other properties ... public required Guid TenantId { get; set; } }

Second, we need to update the DbContext:

csharp
public class ApplicationDbContext : DbContext { private readonly Guid? _currentTenantId; public DbSet<Author> Authors { get; set; } = default!; public DbSet<Book> Books { get; set; } = default!; public ApplicationDbContext( DbContextOptions<ApplicationDbContext> options, ITenantService tenantService) : base(options) { _currentTenantId = tenantService.GetCurrentTenantId(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Book>() .HasQueryFilter(x => x.TenantId == _currentTenantId); base.OnModelCreating(modelBuilder); // Rest of the code remains unchanged } }

In this code, we've updated the query filter and added an x.TenantId == _currentTenantId statement. Since we're creating a DbContext per request, we can inject the current tenant id from the request (the customer identifier accessing data in our application).

Here's a simple tenant service implementation that retrieves a tenant id from HTTP request headers:

csharp
public interface ITenantService { Guid? GetCurrentTenantId(); } public class TenantService : ITenantService { private readonly Guid? _currentTenantId; public TenantService(IHttpContextAccessor accessor) { var headers = accessor.HttpContext?.Request.Headers; _currentTenantId = headers.TryGetValue("Tenant-Id", out var value) is true ? Guid.Parse(value.ToString()) : null; } public Guid? GetCurrentTenantId() => _currentTenantId; }

Now let's create a corresponding minimal API endpoint:

csharp
app.MapGet("/api/tenant-books", async (ApplicationDbContext dbContext) => { var tenantBooks = await dbContext.Books.ToListAsync(); return Results.Ok(tenantBooks); });

On every read query, a tenant ID global query filter is applied to ensure data integrity. As a result, when calling this endpoint, each customer can only retrieve their own books.

Copied

Named Query Filters

In EF Core 10, Global Query Filters were renamed to Named Query Filters.

This update now solves a significant limitation EF Core previously had: support for multiple global query filters on a single entity.

In the application, we might need to have multiple query filters for the same entity:

  • One for soft deletion
  • One for multi-tenancy

Let's explore how we can create two named query filters for the Book entity:

csharp
public class ApplicationDbContext : DbContext { public const string SoftDeleteFilter = nameof(SoftDeleteFilter); public const string TenantFiler = nameof(TenantFiler); // ... protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Book>() .HasQueryFilter(SoftDeleteFilter, x => !x.IsDeleted) .HasQueryFilter(TenantFiler, x => x.TenantId == _currentTenantId); // ... } }

Each time we query books, both filters are applied automatically:

csharp
app.MapGet("/api/tenant-books", async (ApplicationDbContext dbContext) => { var tenantBooks = await dbContext.Books .ToListAsync(); return Results.Ok(tenantBooks); });

This endpoint will return all books that are not deleted and belong to the current tenant.

To ignore a specific filter, we can use the IgnoreQueryFilters method and get all tenant books, including softly deleted ones:

csharp
app.MapGet("/api/tenant-books", async (ApplicationDbContext dbContext) => { var tenantBooks = await dbContext.Books .IgnoreQueryFilters([ApplicationDbContext.SoftDeleteFilter]) .ToListAsync(); return Results.Ok(tenantBooks); });

A good practice is to use constants for the filter names.

Copied

Summary

Global query filters in EF Core is a powerful feature that enforces data access rules consistently across the application.

They are particularly useful in multi-tenant architectures and in scenarios such as soft deletion, ensuring that filter queries are automatically applied to all read operations.

By applying these filters to EF Core entity models, you can significantly simplify your data access code, ensure data integrity and reduce the risk of forgetting to apply important filters to the read operations.

In EF Core 10, they got a nice update: now multiple query filters can be applied to the same entity. You need to name the filters and ignore some of them when needed.

Hope you find this newsletter useful. See you next time.

You can download source code for this newsletter for free
Download source code

Whenever you're ready, here's how I can help you:

The .NET Senior Playbook — 800+ real-world interview questions with expert answers across 50 chapters. You try to answer each question first, then reveal the full solution — and a test after every chapter proves it actually stuck. Finish, and you earn a verifiable certificate for your LinkedIn.

Chapter-test results with a per-answer explanation
View the Playbook

Enjoyed this article? Share it with your network

Improve Your .NET and Architecture Skills

Join my community of 25,000+ developers and architects.

Each week you will get 1 practical tip with best practices and real-world examples.

Learn how to craft better software with source code available for my newsletter.

Join 25,000+ developers already reading
No spam. Unsubscribe any time.