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.
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:
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:
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:
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:
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:
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.
👉 Read the full article on my newsletter: https://antondevtips.com/blog/named-global-query-filters-were-updated-in-ef-core-10
Top comments (0)