- Published on
Named Query Filters in EF Core .NET 10: A Game-Changer for Complex Filtering
- Authors
- Name
- Ivan Gechev
Entity Framework Core has long supported global query filters, a powerful feature that automatically applies filtering logic to all queries for a specific entity type. However, until now, we as developers, have been limited to a single filter per entity type.
With the introduction of .NET 10, EF Core brings us named query filters, revolutionizing how we handle multiple filtering scenarios in our applications.
In this article, we'll explore this exciting new feature using a practical cat adoption service example, demonstrating how named filters solve real-world challenges that previously required workarounds.
The Single Filter Limitation in EF Core Up to .NET 9
Before diving into the new capabilities, let's understand the constraints we've been working with. In EF Core versions up to .NET 9, we could only define one global query filter per entity type. This often forces us to combine multiple filtering conditions into a single, complex expression.
Consider a cat adoption service where we need to filter cats based on two distinct business rules:
- Adoption Status: Only show cats that haven't been adopted yet
- Age Appropriateness: Hide kittens younger than 6 months (too young for adoption)
This goes with the following entity model:
public class Cat
{
public int Id { get; set; }
public required string Name { get; set; }
public required string Breed { get; set; }
public int AgeInMonths { get; set; }
public bool IsAdopted { get; set; }
public DateTime DateOfBirth { get; set; }
public string? SpecialNeeds { get; set; }
}
In the pre-.NET 10 world, we would have been forced to combine these filters:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Cat>()
.HasQueryFilter(c => !c.IsAdopted && c.AgeInMonths >= 6);
}
While this works, it creates two major problems:
- Inflexibility: We can't selectively disable just one of these conditions
- Complexity: As business rules grow, the single filter becomes unwieldy
Next, we'll build a complete example to see how .NET 10's named filters solve these issues.
Building Our Cat Adoption Service
We already have our Cat
entity defined. Now, let us set up our AdoptionDbContext
class:
public class AdoptionDbContext(DbContextOptions options) : DbContext(options)
{
public DbSet<Cat> Cats { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Cat>()
.HasQueryFilter("IsAdopted", c => !c.IsAdopted)
.HasQueryFilter("IsTooYoungForAdoption", c => c.AgeInMonths >= 6);
base.OnModelCreating(modelBuilder);
}
}
Here, we've defined two named query filters:
IsAdopted
: Filters out adopted catsIsTooYoungForAdoption
: Filters out kittens younger than 6 months With these named filters, we can now apply them independently in our queries.
Now, let's move to the Program.cs
file to set up our application:
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AdoptionDbContext>(options =>
options.UseInMemoryDatabase("CatAdoptionDb"));
var app = builder.Build();
using (var scope = app.Services.CreateScope())
{
var context = scope.ServiceProvider
.GetRequiredService<AdoptionDbContext>();
await SeedDatabase(context);
}
app.Run();
static async Task SeedDatabase(AdoptionDbContext context)
{
if (await context.Cats.AnyAsync())
{
return;
}
var cats = new List<Cat>
{
new()
{
Name = "Luna",
Breed = "Siamese",
AgeInMonths = 12,
IsAdopted = false,
DateOfBirth = DateTime.Now.AddMonths(-12)
},
new()
{
Name = "Whiskers",
Breed = "Persian",
AgeInMonths = 24,
IsAdopted = true,
DateOfBirth = DateTime.Now.AddMonths(-24)
},
new()
{
Name = "Mittens",
Breed = "Maine Coon",
AgeInMonths = 8,
IsAdopted = false,
DateOfBirth = DateTime.Now.AddMonths(-8)
},
new()
{
Name = "Snowball",
Breed = "Ragdoll",
AgeInMonths = 3,
IsAdopted = false,
DateOfBirth = DateTime.Now.AddMonths(-3),
SpecialNeeds = "Requires daily medication"
},
new()
{
Name = "Shadow",
Breed = "British Shorthair",
AgeInMonths = 18,
IsAdopted = true,
DateOfBirth = DateTime.Now.AddMonths(-18)
},
new()
{
Name = "Ginger",
Breed = "Tabby",
AgeInMonths = 4,
IsAdopted = false,
DateOfBirth = DateTime.Now.AddMonths(-4)
},
new()
{
Name = "Felix",
Breed = "Tuxedo",
AgeInMonths = 36,
IsAdopted = false,
DateOfBirth = DateTime.Now.AddMonths(-36)
}
};
context.Cats.AddRange(cats);
await context.SaveChangesAsync();
}
Here, we've set up an in-memory database and seeded it with some sample cat data. This simplifies our work and allows us to test our filters effectively.
Next, we'll define our API endpoints to demonstrate the use of named query filters.
Utilizing Named Query Filters in .NET 10
Now that we have our database context and data set up, let's create some API endpoints to demonstrate how we can use the named query filters.
app.MapGet("/api/cats/available", async (AdoptionDbContext context) =>
{
var availableCats = await context.Cats.ToListAsync();
return Results.Ok(availableCats);
});
In this endpoint, we retrieve all cats that are available for adoption. By default, both named filters (IsAdopted
and IsTooYoungForAdoption
) are applied, so we only get the cats that are not adopted and are older than 6 months.
When calling this endpoint, the response will include: Luna, Mittens, and Felix - the cats that are available for adoption.
app.MapGet("/api/cats/all-ages", async (AdoptionDbContext context) =>
{
var cats = await context.Cats
.IgnoreQueryFilters(["IsTooYoungForAdoption"])
.ToListAsync();
return Results.Ok(cats);
});
In this endpoint, we retrieve all cats regardless of their age. By using the IgnoreQueryFilters()
method and specifying the IsTooYoungForAdoption
filter, we effectively disable that specific filter while keeping the IsAdopted
filter active. This means we will get all non-adopted cats, including kittens younger than 6 months.
here.
If you are interested in having a deeper dive into Minimal APIs you can check my Minimal APIs in ASP.NET Core course
When calling this endpoint, the response will include: Luna, Mittens, Snowball, Ginger, Felix - all non-adopted cats regardless of age.
app.MapGet("/api/cats/including-adopted", async (AdoptionDbContext context) =>
{
var cats = await context.Cats
.IgnoreQueryFilters(["IsAdopted"])
.ToListAsync();
return Results.Ok(cats);
});
With our next endpoint, we can retrieve all cats regardless of their adoption status. We achieve this by again using using IgnoreQueryFilters()
, but this time we specify the IsAdopted
filter. This disables that specific filter while keeping the IsTooYoungForAdoption
filter active. In other words, we will get all cats older than 6 months, including those that have already been adopted.
When calling this endpoint, the response will include: Luna, Whiskers, Mittens, Shadow, Felix - all cats older than 6 months regardless of adoption status.
app.MapGet("/api/cats/all", async (AdoptionDbContext context) =>
{
var allCats = await context.Cats.IgnoreQueryFilters().ToListAsync();
return Results.Ok(allCats);
});
Finally, we have an endpoint that retrieves all cats in the database, completely ignoring all named query filters. We achieve this by calling the IgnoreQueryFilters()
method without passing any arguments. This is useful for administrative purposes where we need to see the full list of cats without any filtering.
When calling this endpoint, the response will include: Luna, Whiskers, Mittens, Snowball, Shadow, Ginger, Felix - every cat in the database.
Best Practices
When implementing named query filters, consider these practices:
Use Descriptive Names: Choose filter names that clearly indicate their purpose. "SoftDeletionFilter" is better than "Filter1".
Group Related Logic: If you have multiple conditions that always work together, consider keeping them in a single named filter rather than splitting them unnecessarily.
Clearly Document Filter Interactions: Make sure your team understands how different filters interact, especially when dealing with related entities.
Conclusion
Named query filters in EF Core .NET 10 transform how we handle complex filtering scenarios. By allowing multiple, independently manageable filters per entity type, this feature eliminates the compromises and workarounds that developers have endured for years. As you plan your next EF Core project or consider upgrading existing ones, named query filters should definitely be on your radar. They represent not just a new feature, but a fundamental improvement in how we can architect our data access layer.