EF Core 10 — Named Query Filters, JSON Columns, and Bulk Updates That Actually Work
Three EF Core 10 features that quietly fix long-running pain. Plus the migration trap on each one.
EF Core 10's release notes look modest until you've been bitten by the things it fixes. Three features I've already adopted on production services, and the gotcha for each.
Named query filters.
The classic problem: a HasQueryFilter(x => x.TenantId == _ctx.TenantId) on every entity. When you need to bypass it for an admin tool, you call IgnoreQueryFilters(), which kills all filters, including the soft-delete one. You've now exposed deleted records to the admin tool.
10 lets you name filters:
modelBuilder.Entity<Order>()
.HasQueryFilter("Tenant", e => e.TenantId == _ctx.TenantId)
.HasQueryFilter("SoftDelete", e => !e.IsDeleted);
// Admin tool: bypass only tenant
db.Orders.IgnoreQueryFilters("Tenant").Where(...);
This is a safety upgrade, not a convenience. The old all-or-nothing pattern produced real data leaks. Use named filters from now on. The migration trap: existing global filters need to be renamed and references audited. Don't auto-migrate without reviewing every IgnoreQueryFilters() callsite.
JSON columns with bulk updates.
EF Core has had JsonProperty for a while, but bulk-updating JSON values required raw SQL. 10 wires ExecuteUpdateAsync into JSON paths:
await db.Users
.Where(u => u.Settings.Theme == "light")
.ExecuteUpdateAsync(s => s.SetProperty(u => u.Settings.Theme, "dark"));
Generates one UPDATE statement. No load-modify-save. Migration trap: the SQL Server JSON path syntax and the Postgres jsonb syntax produce slightly different SQL. Indexing on JSON paths is provider-specific — on Postgres, add a jsonb_path_ops GIN index for the columns you query; on SQL Server, computed columns plus indexes are still the pattern. Don't assume cross-provider parity in performance.
Complex types.
OwnsOne used to be the awkward way to embed a value object (address, money) into an entity. Complex types in EF Core 10 are the cleaner replacement — no shadow primary key, no implicit dependent entity. The mapping reads like an actual value object:
modelBuilder.Entity<Order>().ComplexProperty(o => o.ShipTo);
Migration trap: switching an existing OwnsOne to ComplexProperty generates a destructive migration if you're not careful, because EF sees it as removing one mapping and adding another. Review the generated migration, don't just run it.
The thing that didn't quite land in 10: AOT-friendly query compilation is much improved but still has rough edges with reflection-heavy queries. The path works for the simple-to-medium case. Stress-test your hottest queries before assuming it works for everything.
The migration shape I'd recommend: bump to EF Core 10, adopt named filters first (immediate safety win), then bulk JSON updates where you have them, then complex types when you next touch the affected entities. Don't try to do all three at once on a busy service.