Every EF Core release adds query translation improvements and a handful of genuinely useful new features. EF Core 10 is no different — but named query filters stand out as a feature that solves a real architectural problem that developers have worked around for years. Combined with LINQ enhancements and improved JSON column support, this is a meaningful upgrade.
This post covers every significant EF Core 10 change with code you can use immediately.
Table of Contents
- Named Query Filters
- LINQ Translation Enhancements
- JSON Column Improvements
- Complex Type Improvements
- Raw SQL Enhancements
- Migration Improvements
- Performance Changes
- Breaking Changes
- FAQ
- Conclusion
Named Query Filters
This is the headline feature of EF Core 10. Previously, query filters were anonymous — you could apply one per entity type and disable it globally with IgnoreQueryFilters(). If you had multiple concerns (soft delete + tenant isolation), you couldn’t disable just one.
The Old Problem
// EF Core 9 and earlier — one filter per entity
modelBuilder.Entity<Post>().HasQueryFilter(p => !p.IsDeleted && p.TenantId == _tenantId);
// Disabling one meant disabling both — no granularity
var posts = context.Posts
.IgnoreQueryFilters() // Disables BOTH soft delete AND tenant filter
.ToList();
EF Core 10: Named Filters
// Define multiple named filters per entity
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasQueryFilter("softDelete", p => !p.IsDeleted)
.HasQueryFilter("tenantIsolation", p => p.TenantId == _currentTenantId);
modelBuilder.Entity<Comment>()
.HasQueryFilter("softDelete", c => !c.IsDeleted);
}
// Disable only one filter
var allPostsForTenant = context.Posts
.IgnoreQueryFilter("softDelete") // Tenant filter still active
.ToList();
// Disable a specific filter globally on a query
var adminView = context.Posts
.IgnoreQueryFilter("tenantIsolation")
.IgnoreQueryFilter("softDelete")
.ToList();
// Or ignore all (backward compatible)
context.Posts.IgnoreQueryFilters().ToList();
Real-World Pattern: Multi-Tenant SaaS
public class ApplicationDbContext : DbContext
{
private readonly ITenantService _tenantService;
public ApplicationDbContext(DbContextOptions options, ITenantService tenantService)
: base(options)
{
_tenantService = tenantService;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var tenantId = _tenantService.CurrentTenantId;
// Apply to all tenant-scoped entities
foreach (var entityType in modelBuilder.Model.GetEntityTypes()
.Where(e => typeof(ITenantEntity).IsAssignableFrom(e.ClrType)))
{
modelBuilder.Entity(entityType.ClrType)
.HasQueryFilter("tenant", BuildTenantFilter(entityType.ClrType, tenantId));
}
// Apply soft delete to all soft-deletable entities
foreach (var entityType in modelBuilder.Model.GetEntityTypes()
.Where(e => typeof(ISoftDeletable).IsAssignableFrom(e.ClrType)))
{
modelBuilder.Entity(entityType.ClrType)
.HasQueryFilter("softDelete", BuildSoftDeleteFilter(entityType.ClrType));
}
}
}
LINQ Translation Enhancements
ExecuteUpdate with Navigation Properties
EF Core 10 extends ExecuteUpdate to support navigation property access — previously you couldn’t reference navigation properties in bulk update expressions:
// EF Core 10 — update using navigation property
await context.Orders
.Where(o => o.Customer.Tier == CustomerTier.Premium)
.ExecuteUpdateAsync(s => s
.SetProperty(o => o.DiscountPercent, 15)
.SetProperty(o => o.UpdatedAt, DateTime.UtcNow));
GroupBy with Complex Projections
More GroupBy patterns now translate to SQL instead of evaluating client-side:
// Now translates to SQL in EF Core 10
var categorySummary = await context.Products
.GroupBy(p => p.CategoryId)
.Select(g => new
{
CategoryId = g.Key,
Count = g.Count(),
AveragePrice = g.Average(p => p.Price),
MaxPrice = g.Max(p => p.Price),
MinPrice = g.Min(p => p.Price),
// New in EF Core 10: string_agg equivalent
ProductNames = string.Join(", ", g.Select(p => p.Name))
})
.ToListAsync();
DateOnly and TimeOnly Improvements
// More DateOnly/TimeOnly methods now translate to SQL
var recentOrders = await context.Orders
.Where(o => DateOnly.FromDateTime(o.CreatedAt) >= DateOnly.FromDateTime(DateTime.Today.AddDays(-30)))
.Where(o => TimeOnly.FromDateTime(o.CreatedAt).IsBetween(
new TimeOnly(9, 0),
new TimeOnly(17, 0)))
.ToListAsync();
Improved Math Function Translation
// Math functions now translate to native SQL functions
var products = await context.Products
.Select(p => new
{
p.Name,
RoundedPrice = Math.Round(p.Price, 2),
LogPrice = Math.Log(p.Price),
SqrtWeight = Math.Sqrt(p.WeightGrams)
})
.ToListAsync();
JSON Column Improvements
EF Core 8 introduced JSON column mapping. EF Core 10 refines query translation for complex JSON scenarios.
JSON Collection Filtering
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
// Stored as JSON column
public List<ProductTag> Tags { get; set; } = new();
public ProductMetadata Metadata { get; set; } = new();
}
public class ProductTag
{
public string Name { get; set; } = string.Empty;
public string Color { get; set; } = string.Empty;
}
// EF Core 10 — filter on JSON array contents
var taggedProducts = await context.Products
.Where(p => p.Tags.Any(t => t.Name == "sale"))
.ToListAsync();
// Filter on JSON object properties
var highValueProducts = await context.Products
.Where(p => p.Metadata.ViewCount > 1000 && p.Metadata.Rating >= 4.0)
.OrderByDescending(p => p.Metadata.Rating)
.ToListAsync();
JSON Column Configuration
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Product>()
.OwnsMany(p => p.Tags, tags =>
{
tags.ToJson(); // Store as JSON array column
});
modelBuilder.Entity<Product>()
.OwnsOne(p => p.Metadata, meta =>
{
meta.ToJson(); // Store as JSON object column
});
}
New: JSON Partial Updates
EF Core 10 improves change tracking for JSON columns — updating a single property inside a JSON object now generates a targeted JSON_MODIFY SQL command instead of replacing the entire JSON column value:
var product = await context.Products.FindAsync(1);
product.Metadata.Rating = 4.8; // Only the Rating property is updated in SQL
await context.SaveChangesAsync();
// Generates: UPDATE Products SET Metadata = JSON_MODIFY(Metadata, '$.Rating', 4.8)
Complex Type Improvements
EF Core 8 introduced complex types (value objects without identity). EF Core 10 improves their usability.
Complex Type Collections
// Complex type — no PK, value-based equality
[ComplexType]
public class Address
{
public string Street { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
public string PostalCode { get; set; } = string.Empty;
public string Country { get; set; } = string.Empty;
}
public class Customer
{
public int Id { get; set; }
public Address BillingAddress { get; set; } = new();
public Address ShippingAddress { get; set; } = new();
// EF Core 10: collections of complex types
public List<Address> PreviousAddresses { get; set; } = new();
}
// Query on complex type properties (improved translation in EF Core 10)
var ukCustomers = await context.Customers
.Where(c => c.BillingAddress.Country == "UK")
.ToListAsync();
Complex Type Equality in Queries
var targetAddress = new Address { City = "London", Country = "UK" };
// EF Core 10 — query by complex type equality
var customers = await context.Customers
.Where(c => c.ShippingAddress == targetAddress)
.ToListAsync();
// Translates to: WHERE ShippingAddress_City = 'London' AND ShippingAddress_Country = 'UK'
Raw SQL Enhancements
SqlQuery for Non-Entity Types
// Return arbitrary types from raw SQL (improved in EF Core 10)
var summary = await context.Database
.SqlQuery<OrderSummary>($"""
SELECT
c.Name AS CustomerName,
COUNT(o.Id) AS OrderCount,
SUM(o.Total) AS TotalSpent
FROM Customers c
JOIN Orders o ON o.CustomerId = c.Id
GROUP BY c.Id, c.Name
ORDER BY TotalSpent DESC
""")
.ToListAsync();
public class OrderSummary
{
public string CustomerName { get; set; } = string.Empty;
public int OrderCount { get; set; }
public decimal TotalSpent { get; set; }
}
FromSql with Composable Queries
// Compose LINQ on top of raw SQL
var expensiveOrders = await context.Orders
.FromSql($"SELECT * FROM Orders WITH (NOLOCK)")
.Where(o => o.Total > 1000 && o.Status == OrderStatus.Completed)
.Include(o => o.Customer)
.OrderByDescending(o => o.Total)
.Take(50)
.ToListAsync();
Migration Improvements
Migration Bundles Enhancements
Migration bundles (self-contained executables for running migrations) support idempotent execution more reliably in EF Core 10:
# Generate migration bundle
dotnet ef migrations bundle --self-contained -r linux-x64 --output ./deploy/migrate
# Run migrations (CI/CD pipeline)
./deploy/migrate --connection "Server=prod-db;Database=MyApp;..."
Improved SQL Generation
EF Core 10 generates cleaner migration SQL — fewer redundant operations when multiple model changes are combined in one migration, particularly for index changes on large tables.
Performance Changes
- Compiled models — startup time improved when using pre-compiled models (
dotnet ef dbcontext optimize). Recommended for large models (50+ entities) - Query plan caching — more query shapes are cached, reducing plan compilation overhead for parameterized queries
- Change tracking memory — reduced memory allocation per tracked entity in large bulk operation scenarios
# Generate compiled model (run once, check in generated files)
dotnet ef dbcontext optimize --output-dir CompiledModels --namespace MyApp.Data.CompiledModels
// Use in DbContext
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseModel(CompiledModels.ApplicationDbContextModel.Instance);
}
Breaking Changes
IgnoreQueryFilters()— now returns a new overload signature; existing calls without arguments continue to work but you may see ambiguity warnings if you have extension methods onIQueryable- Owned entity JSON serialization — some edge cases in JSON column null handling changed; test owned entity queries after upgrading
- GroupBy client evaluation — more operations now throw instead of silently evaluating client-side (this is a correctness fix, but may surface previously-hidden performance issues)
FAQ
Can I use named query filters with Owned entities?
Named query filters apply to root entity types. Owned entities inherit their owner’s filters. You can’t define independent named filters on owned types, but filtering through the owner’s filter covers most scenarios.
Do JSON column queries work on all databases?
JSON column support is provider-specific. SQL Server, PostgreSQL (via Npgsql), and SQLite all support JSON columns with EF Core. MySQL/MariaDB support via Pomelo is in preview for some JSON features.
Is EF Core 10 a direct upgrade from EF Core 8?
Yes — EF Core follows .NET’s versioning and ships with each major .NET release. Upgrade from EF Core 8 to 10 by updating the NuGet package and fixing any breaking changes. EF Core doesn’t require migrating to intermediate versions.
When should I use raw SQL vs LINQ in EF Core?
Use LINQ for standard CRUD and filtered queries — EF Core’s translation is reliable and change tracking works automatically. Use raw SQL for complex reporting queries, CTEs, window functions, or database-specific features that LINQ can’t express cleanly. Composing LINQ on top of FromSql gives you the best of both.
Does EF Core 10 support AOT compilation?
Partial support — compiled models (dbcontext optimize) enable most AOT scenarios. Full NativeAOT support for EF Core is still in progress, with complete support targeted for a future release.
Conclusion
Named query filters are the EF Core 10 feature that will change how you architect multi-tenant and soft-delete systems — the ability to disable individual filters on a per-query basis solves a long-standing design friction. The LINQ translation improvements mean fewer queries falling back to client evaluation, which is a silent performance win for existing codebases.
Upgrade path: update the NuGet package, run your test suite, check for the GroupBy client evaluation breaking change, and adopt named filters at your own pace — it’s a non-breaking addition.
[INTERNAL_LINK: ASP.NET Core Minimal APIs vs Controllers] [INTERNAL_LINK: .NET Aspire explained for .NET developers]

Leave a Reply